Compare commits

...

51 Commits

Author SHA1 Message Date
Charlie Marsh
9d136de55a Bump version to 0.0.121 2022-11-15 16:18:39 -05:00
Harutaka Kawamura
1821c07367 Implement B020 (#753) 2022-11-15 16:17:03 -05:00
Charlie Marsh
1fe90ef7f4 Only notify once for each app update (#762) 2022-11-15 16:14:10 -05:00
Charlie Marsh
b5cb9485f6 Move updater to its own module 2022-11-15 15:51:24 -05:00
Charlie Marsh
4d798512b1 Only print version checks on tty (#761) 2022-11-15 15:36:06 -05:00
Charlie Marsh
5f9815b103 Disable auto-updates in JSON mode (#760) 2022-11-15 15:29:10 -05:00
Charlie Marsh
0d3fac1bf9 Add --line-length command line argument (#759) 2022-11-15 12:23:38 -05:00
Charlie Marsh
ff0e5f5cb4 Preserve scopes when checking deferred strings (#758) 2022-11-15 12:19:22 -05:00
Charlie Marsh
374d57d822 Limit PEP 604 checks to Python 3.10+ (#757) 2022-11-15 11:52:12 -05:00
Edgar R. M
85b2a9920f docs: Add flake8-bandit to ToC (#750) 2022-11-15 00:11:39 -05:00
Charlie Marsh
3c22913470 Bump version to 0.0.120 2022-11-14 22:53:36 -05:00
Charlie Marsh
ea03a59b72 De-alias Literal checks (#748) 2022-11-14 22:53:23 -05:00
Charlie Marsh
058a5276b0 Bump version to 0.0.119 2022-11-14 21:45:41 -05:00
Charlie Marsh
62d4096be3 Move bindings to FNV map (#747) 2022-11-14 21:42:57 -05:00
Charlie Marsh
8961da7b89 Add support for import alias tracking (#746) 2022-11-14 21:29:30 -05:00
Brett Cannon
58bcffbe2d Add isort to the README's ToC (#745) 2022-11-14 18:51:39 -05:00
Charlie Marsh
f67727b13c Improve performance of import matching code (#744) 2022-11-14 17:14:22 -05:00
Charlie Marsh
fea029ae35 Bump version to 0.0.118 2022-11-14 13:21:27 -05:00
Harutaka Kawamura
3e3c3c7421 Ignore namedtuple assignment in N806, N815, and N816 (#735) 2022-11-14 13:21:04 -05:00
Harutaka Kawamura
9047bf680d Implement B024 and B027 (#738) 2022-11-14 13:12:23 -05:00
Charlie Marsh
d170388b7b Allow second line as 'first line' for punctuation (#741) 2022-11-14 13:07:27 -05:00
Charlie Marsh
502d3316f9 Add flake8-bugbear settings to hash (#739) 2022-11-14 12:29:47 -05:00
Harutaka Kawamura
a8159f9893 Implement B022 (#734) 2022-11-14 09:24:05 -05:00
Charlie Marsh
71f727c380 Use FNV hasher in more places (#732) 2022-11-13 23:44:16 -05:00
Charlie Marsh
ce3c45a361 Make combine-as-imports the default import sorting behavior (#731) 2022-11-13 23:07:13 -05:00
Charlie Marsh
29ae6c159d Add FastAPI to README (#730) 2022-11-13 22:27:35 -05:00
Charlie Marsh
1ae07b4c70 Allow explicit re-export of straight imports (#729) 2022-11-13 22:26:48 -05:00
Charlie Marsh
08ca8788a7 Bump version to 0.0.117 2022-11-13 16:10:29 -05:00
Charlie Marsh
8a97a76038 Make # noqa detection case-insensitive (#728) 2022-11-13 16:09:44 -05:00
Brett Cannon
d2d84cf5bf Fix Markdown in README (#727) 2022-11-13 15:19:25 -05:00
Anders Kaseorg
450970e0e6 Restore clippy on all crates in the workspace (#725) 2022-11-13 15:18:57 -05:00
Charlie Marsh
34ecc69914 Don't mark re-exported symbols as unused (#724) 2022-11-13 14:31:43 -05:00
Charlie Marsh
a310aed128 Bump version to 0.0.116 2022-11-13 13:46:05 -05:00
Harutaka Kawamura
43cc8bc84e Lint test code (#721) 2022-11-13 11:55:57 -05:00
Harutaka Kawamura
84bf36194b Implement B012 (#718) 2022-11-13 11:55:33 -05:00
Harutaka Kawamura
e4d168bb4f Implement B021 (#719) 2022-11-13 11:40:24 -05:00
Charlie Marsh
439642addf Regenerate README 2022-11-13 10:55:14 -05:00
Charlie Marsh
f5b1f957e3 Improve some import tracking code (#715) 2022-11-13 00:10:13 -05:00
Charlie Marsh
8f99705795 Make some error messages more grammatically consistent 2022-11-12 16:57:23 -05:00
Charlie Marsh
9ec7e6bcd6 Add function name to B008 message 2022-11-12 16:53:13 -05:00
Charlie Marsh
695b06ba60 Bump version to 0.0.115 2022-11-12 16:46:26 -05:00
Charlie Marsh
3a2e6926d4 Include flake8-bugbear settings in flake8-to-ruff (#712) 2022-11-12 16:46:12 -05:00
Charlie Marsh
d16c3a1186 Use an FNVHashSet for settings.enabled (#711) 2022-11-12 16:36:56 -05:00
Charlie Marsh
53a2187f02 Run cargo fmt 2022-11-12 16:33:12 -05:00
Charlie Marsh
00b5d1059c Validate that mutable and immutable defaults are imported (#710) 2022-11-12 16:32:21 -05:00
Charlie Marsh
b7acf76aaf Track all import-from members (#709) 2022-11-12 16:10:43 -05:00
Charlie Marsh
8cfc0e5cf5 Use FnvHasher for unordered maps and sets (#708) 2022-11-12 16:09:34 -05:00
Edgar R. M
aa7681f9ad Add extend-immutable-calls setting for B008 (#706) 2022-11-12 15:48:34 -05:00
Charlie Marsh
2493d48725 Add flake8-bandit to flake8-to-ruff (#701) 2022-11-12 12:08:15 -05:00
Edgar R. M
1b422a7f12 Add flake8-bandit (#697) 2022-11-12 12:04:49 -05:00
Charlie Marsh
da051624e4 Add backticks around functools.lru_cache 2022-11-12 11:56:23 -05:00
128 changed files with 3990 additions and 867 deletions

View File

@@ -77,7 +77,7 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- run: cargo clippy --all -- -D warnings
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo_test:
name: "cargo test"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.114
rev: v0.0.121
hooks:
- id: ruff

9
Cargo.lock generated
View File

@@ -930,11 +930,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.114-dev.0"
version = "0.0.121-dev.0"
dependencies = [
"anyhow",
"clap 4.0.22",
"configparser",
"fnv",
"once_cell",
"regex",
"ruff",
@@ -2237,10 +2238,11 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.114"
version = "0.0.121"
dependencies = [
"anyhow",
"assert_cmd",
"atty",
"bincode",
"bitflags",
"cacache",
@@ -2253,6 +2255,7 @@ dependencies = [
"dirs 4.0.0",
"fern",
"filetime",
"fnv",
"getrandom 0.2.8",
"glob",
"insta",
@@ -2284,7 +2287,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.114"
version = "0.0.121"
dependencies = [
"anyhow",
"clap 4.0.22",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.114"
version = "0.0.121"
edition = "2021"
[lib]
@@ -14,6 +14,7 @@ name = "ruff"
[dependencies]
anyhow = { version = "1.0.66" }
atty = { version = "0.2.14" }
bincode = { version = "1.3.3" }
bitflags = { version = "1.3.2" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }
@@ -23,6 +24,7 @@ common-path = { version = "1.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.17" }
fnv = { version = "1.0.7" }
glob = { version = "0.3.0" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "a13ec97dd4eb925bde4d426c6e422582793b260c" }

View File

@@ -36,8 +36,8 @@ faster than any individual tool.
automatically convert your existing configuration.)
Ruff is actively developed and used in major open-source projects
like [Zulip](https://github.com/zulip/zulip), [pydantic](https://github.com/pydantic/pydantic),
and [Saleor](https://github.com/saleor/saleor).
like [FastAPI](https://github.com/tiangolo/fastapi), [Zulip](https://github.com/zulip/zulip),
[pydantic](https://github.com/pydantic/pydantic), and [Saleor](https://github.com/saleor/saleor).
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -48,18 +48,20 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
3. [Supported Rules](#supported-rules)
1. [Pyflakes](#pyflakes)
2. [pycodestyle](#pycodestyle)
3. [pydocstyle](#pydocstyle)
4. [pyupgrade](#pyupgrade)
5. [pep8-naming](#pep8-naming)
6. [flake8-comprehensions](#flake8-comprehensions)
7. [flake8-bugbear](#flake8-bugbear)
8. [flake8-builtins](#flake8-builtins)
9. [flake8-print](#flake8-print)
10. [flake8-quotes](#flake8-quotes)
11. [flake8-annotations](#flake8-annotations)
12. [flake8-2020](#flake8-2020)
13. [Ruff-specific rules](#ruff-specific-rules)
14. [Meta rules](#meta-rules)
3. [isort](#isort)
4. [pydocstyle](#pydocstyle)
5. [pyupgrade](#pyupgrade)
6. [pep8-naming](#pep8-naming)
7. [flake8-bandit](#flake8-bandit)
8. [flake8-comprehensions](#flake8-comprehensions)
9. [flake8-bugbear](#flake8-bugbear)
10. [flake8-builtins](#flake8-builtins)
11. [flake8-print](#flake8-print)
12. [flake8-quotes](#flake8-quotes)
13. [flake8-annotations](#flake8-annotations)
14. [flake8-2020](#flake8-2020)
15. [Ruff-specific rules](#ruff-specific-rules)
16. [Meta rules](#meta-rules)
5. [Editor Integrations](#editor-integrations)
6. [FAQ](#faq)
7. [Development](#development)
@@ -99,7 +101,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.114
rev: v0.0.116
hooks:
- id: ruff
```
@@ -235,6 +237,8 @@ Options:
Regular expression matching the name of dummy variables
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
--line-length <LINE_LENGTH>
Set the line-length for length-associated checks and automatic formatting
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
-h, --help
@@ -335,7 +339,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
| F702 | ContinueOutsideLoop | `continue` not properly in loop | |
| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function | |
| F706 | ReturnOutsideFunction | `return` statement outside of a function/method | |
| F707 | DefaultExceptNotLast | An `except:` block as not the last exception handler | |
| F707 | DefaultExceptNotLast | An `except` block as not the last exception handler | |
| F722 | ForwardAnnotationSyntaxError | Syntax error in forward annotation: `...` | |
| F821 | UndefinedName | Undefined name `...` | |
| F822 | UndefinedExport | Undefined name `...` in `__all__` | |
@@ -409,7 +413,7 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/6.1.1/) on PyPI.
| D400 | EndsInPeriod | First line should end with a period | |
| D402 | NoSignature | First line should not be the function's signature | |
| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | |
| D404 | NoThisPrefix | First word of the docstring should not be 'This' | |
| D404 | NoThisPrefix | First word of the docstring should not be "This" | |
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | 🛠 |
| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | 🛠 |
| D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | 🛠 |
@@ -440,9 +444,9 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | 🛠 |
| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | 🛠 |
| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | 🛠 |
| U009 | PEP3120UnnecessaryCodingComment | utf-8 encoding declaration is unnecessary | 🛠 |
| U009 | PEP3120UnnecessaryCodingComment | UTF-8 encoding declaration is unnecessary | 🛠 |
| U010 | UnnecessaryFutureImport | Unnecessary `__future__` import `...` for target Python version | 🛠 |
| U011 | UnnecessaryLRUCacheParams | Unnecessary parameters to functools.lru_cache | 🛠 |
| U011 | UnnecessaryLRUCacheParams | Unnecessary parameters to `functools.lru_cache` | 🛠 |
| U012 | UnnecessaryEncodeUTF8 | Unnecessary call to `encode` as UTF-8 | 🛠 |
### pep8-naming
@@ -467,6 +471,19 @@ For more, see [pep8-naming](https://pypi.org/project/pep8-naming/0.13.2/) on PyP
| N817 | CamelcaseImportedAsAcronym | Camelcase `...` imported as acronym `...` | |
| N818 | ErrorSuffixOnExceptionName | Exception name `...` should be named with an Error suffix | |
### flake8-bandit
For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/4.1.1/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| S101 | AssertUsed | Use of `assert` detected | |
| S102 | ExecUsed | Use of `exec` detected | |
| S104 | HardcodedBindAllInterfaces | Possible binding to all interfaces | |
| S105 | HardcodedPasswordString | Possible hardcoded password: `"..."` | |
| S106 | HardcodedPasswordFuncArg | Possible hardcoded password: `"..."` | |
| S107 | HardcodedPasswordDefault | Possible hardcoded password: `"..."` | |
### flake8-comprehensions
For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/3.10.1/) on PyPI.
@@ -496,25 +513,31 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| B002 | UnaryPrefixIncrement | Python does not support the unary prefix increment. | |
| B003 | AssignmentToOsEnviron | Assigning to `os.environ` doesn't clear the environment. | |
| B002 | UnaryPrefixIncrement | Python does not support the unary prefix increment | |
| B003 | AssignmentToOsEnviron | Assigning to `os.environ` doesn't clear the environment | |
| B004 | UnreliableCallableCheck | Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use `callable(x)` for consistent results. | |
| B005 | StripWithMultiCharacters | Using `.strip()` with multi-character strings is misleading the reader. | |
| B006 | MutableArgumentDefault | Do not use mutable data structures for argument defaults. | |
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body. | 🛠 |
| B008 | FunctionCallArgumentDefault | Do not perform function calls in argument defaults. | |
| B009 | GetAttrWithConstant | Do not call `getattr` with a constant attribute value, it is not any safer than normal property access. | 🛠 |
| B010 | SetAttrWithConstant | Do not call `setattr` with a constant attribute value, it is not any safer than normal property access. | |
| B005 | StripWithMultiCharacters | Using `.strip()` with multi-character strings is misleading the reader | |
| B006 | MutableArgumentDefault | Do not use mutable data structures for argument defaults | |
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body | 🛠 |
| B008 | FunctionCallArgumentDefault | Do not perform function call in argument defaults | |
| B009 | GetAttrWithConstant | Do not call `getattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B010 | SetAttrWithConstant | Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. | |
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError:` instead of `except (ValueError,):`. | |
| B012 | JumpStatementInFinally | `return/continue/break` inside finally blocks cause exceptions to be silenced | |
| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError` instead of `except (ValueError,)`. | |
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 |
| B015 | UselessComparison | Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it. | |
| B016 | CannotRaiseLiteral | Cannot raise a literal. Did you intend to return it or raise an Exception? | |
| B017 | NoAssertRaisesException | `assertRaises(Exception):` should be considered evil. | |
| B017 | NoAssertRaisesException | `assertRaises(Exception)` should be considered evil | |
| B018 | UselessExpression | Found useless expression. Either assign it to a variable or remove it. | |
| B019 | CachedInstanceMethod | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks. | |
| B019 | CachedInstanceMethod | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks | |
| B020 | LoopVariableOverridesIterator | Loop control variable `...` overrides iterable it iterates | |
| B021 | FStringDocstring | f-string used as docstring. This will be interpreted by python as a joined string rather than a docstring. | |
| B022 | UselessContextlibSuppress | No arguments passed to `contextlib.suppress`. No exceptions will be suppressed and therefore this context manager is redundant | |
| B024 | AbstractBaseClassWithoutAbstractMethod | `...` is an abstract base class, but it has no abstract methods | |
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged. | |
| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged | |
| B027 | EmptyMethodWithoutAbstractDecorator | `...` is an empty method in an abstract base class, but has no abstract decorator | |
### flake8-builtins
@@ -686,7 +709,8 @@ including:
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (21/32)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (25/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (15/34)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
@@ -709,8 +733,9 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (21/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (26/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
Ruff can also replace [`isort`](https://pypi.org/project/isort/), [`yesqa`](https://github.com/asottile/yesqa),
@@ -736,9 +761,8 @@ project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
### How does Ruff's import sorting compare to [`isort`](https://pypi.org/project/isort/)?
Ruff's import sorting is intended to be equivalent to `isort` when used `profile = "black"`, and a
few other settings (`combine_as_imports = true`, `order_by_type = false`, and
`case_sensitive` = true`).
Ruff's import sorting is intended to be nearly equivalent to `isort` when used `profile = "black"`.
(There are some minor differences in how Ruff and isort break ties between similar imports.)
Like `isort`, Ruff's import sorting is compatible with Black.

View File

@@ -1,11 +1,11 @@
use std::fs;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use ropey::Rope;
use ruff::fs;
fn criterion_benchmark(c: &mut Criterion) {
let contents = fs::read_file(Path::new("resources/test/fixtures/D.py")).unwrap();
let contents = fs::read_to_string(Path::new("resources/test/fixtures/D.py")).unwrap();
c.bench_function("rope", |b| {
b.iter(|| {
let rope = Rope::from_str(black_box(&contents));

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.114"
version = "0.0.121"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.114"
version = "0.0.121"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.114-dev.0"
version = "0.0.121-dev.0"
edition = "2021"
[lib]
@@ -10,6 +10,7 @@ name = "flake8_to_ruff"
anyhow = { version = "1.0.66" }
clap = { version = "4.0.1", features = ["derive"] }
configparser = { version = "3.0.2" }
fnv = { version = "1.0.7" }
once_cell = { version = "1.16.0" }
regex = { version = "1.6.0" }
ruff = { path = "..", default-features = false }

View File

@@ -5,7 +5,7 @@ use ruff::checks_gen::CheckCodePrefix;
use ruff::flake8_quotes::settings::Quote;
use ruff::settings::options::Options;
use ruff::settings::pyproject::Pyproject;
use ruff::{flake8_annotations, flake8_quotes, pep8_naming};
use ruff::{flake8_annotations, flake8_bugbear, flake8_quotes, pep8_naming};
use crate::plugin::Plugin;
use crate::{parser, plugin};
@@ -69,6 +69,7 @@ pub fn convert(
// Parse each supported option.
let mut options: Options = Default::default();
let mut flake8_annotations: flake8_annotations::settings::Options = Default::default();
let mut flake8_bugbear: flake8_bugbear::settings::Options = Default::default();
let mut flake8_quotes: flake8_quotes::settings::Options = Default::default();
let mut pep8_naming: pep8_naming::settings::Options = Default::default();
for (key, value) in flake8 {
@@ -109,6 +110,11 @@ pub fn convert(
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
}
}
// flake8-bugbear
"extend-immutable-calls" | "extend_immutable_calls" => {
flake8_bugbear.extend_immutable_calls =
Some(parser::parse_strings(value.as_ref()));
}
// flake8-annotations
"suppress-none-returning" | "suppress_none_returning" => {
match parser::parse_bool(value.as_ref()) {
@@ -179,6 +185,12 @@ pub fn convert(
// Deduplicate and sort.
options.select = Some(Vec::from_iter(select));
options.ignore = Some(Vec::from_iter(ignore));
if flake8_annotations != Default::default() {
options.flake8_annotations = Some(flake8_annotations);
}
if flake8_bugbear != Default::default() {
options.flake8_bugbear = Some(flake8_bugbear);
}
if flake8_quotes != Default::default() {
options.flake8_quotes = Some(flake8_quotes);
}
@@ -224,6 +236,7 @@ mod tests {
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
@@ -257,6 +270,7 @@ mod tests {
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
@@ -290,6 +304,7 @@ mod tests {
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
@@ -323,6 +338,7 @@ mod tests {
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
@@ -356,6 +372,7 @@ mod tests {
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
@@ -432,6 +449,7 @@ mod tests {
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
@@ -466,6 +484,7 @@ mod tests {
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,

View File

@@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use std::str::FromStr;
use anyhow::Result;
use fnv::FnvHashMap;
use once_cell::sync::Lazy;
use regex::Regex;
use ruff::checks_gen::CheckCodePrefix;
@@ -179,8 +179,8 @@ pub fn parse_files_to_codes_mapping(value: &str) -> Result<Vec<PatternPrefixPair
/// Collect a list of `PatternPrefixPair` structs as a `BTreeMap`.
pub fn collect_per_file_ignores(
pairs: Vec<PatternPrefixPair>,
) -> BTreeMap<String, Vec<CheckCodePrefix>> {
let mut per_file_ignores: BTreeMap<String, Vec<CheckCodePrefix>> = BTreeMap::new();
) -> FnvHashMap<String, Vec<CheckCodePrefix>> {
let mut per_file_ignores: FnvHashMap<String, Vec<CheckCodePrefix>> = FnvHashMap::default();
for pair in pairs {
per_file_ignores
.entry(pair.pattern)

View File

@@ -6,6 +6,7 @@ use ruff::checks_gen::CheckCodePrefix;
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Plugin {
Flake8Bandit,
Flake8Bugbear,
Flake8Builtins,
Flake8Comprehensions,
@@ -22,6 +23,7 @@ impl FromStr for Plugin {
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"flake8-bandit" => Ok(Plugin::Flake8Bandit),
"flake8-bugbear" => Ok(Plugin::Flake8Bugbear),
"flake8-builtins" => Ok(Plugin::Flake8Builtins),
"flake8-comprehensions" => Ok(Plugin::Flake8Comprehensions),
@@ -39,6 +41,7 @@ impl FromStr for Plugin {
impl Plugin {
pub fn default(&self) -> CheckCodePrefix {
match self {
Plugin::Flake8Bandit => CheckCodePrefix::S,
Plugin::Flake8Bugbear => CheckCodePrefix::B,
Plugin::Flake8Builtins => CheckCodePrefix::A,
Plugin::Flake8Comprehensions => CheckCodePrefix::C,
@@ -53,6 +56,7 @@ impl Plugin {
pub fn select(&self, flake8: &HashMap<String, Option<String>>) -> Vec<CheckCodePrefix> {
match self {
Plugin::Flake8Bandit => vec![CheckCodePrefix::S],
Plugin::Flake8Bugbear => vec![CheckCodePrefix::B],
Plugin::Flake8Builtins => vec![CheckCodePrefix::A],
Plugin::Flake8Comprehensions => vec![CheckCodePrefix::C],
@@ -265,6 +269,10 @@ pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> V
"docstring-convention" | "docstring_convention" => {
plugins.insert(Plugin::Flake8Docstrings);
}
// flake8-bugbear
"extend-immutable-calls" | "extend_immutable_calls" => {
plugins.insert(Plugin::Flake8Bugbear);
}
// flake8-builtins
"builtins-ignorelist" | "builtins_ignorelist" => {
plugins.insert(Plugin::Flake8Builtins);
@@ -329,6 +337,7 @@ pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> V
/// `flake8-annotations` is active.
pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin> {
[
Plugin::Flake8Bandit,
Plugin::Flake8Bugbear,
Plugin::Flake8Builtins,
Plugin::Flake8Comprehensions,

View File

@@ -0,0 +1,20 @@
from typing import List
import fastapi
from fastapi import Query
def okay(db=fastapi.Depends(get_db)):
...
def okay(data: List[str] = fastapi.Query(None)):
...
def okay(data: List[str] = Query(None)):
...
def error_due_to_missing_import(data: List[str] = Depends(None)):
...

107
resources/test/fixtures/B012.py vendored Normal file
View File

@@ -0,0 +1,107 @@
def a():
try:
pass
finally:
return # warning
def b():
try:
pass
finally:
if 1 + 0 == 2 - 1:
return # warning
def c():
try:
pass
finally:
try:
return # warning
except Exception:
pass
def d():
try:
try:
pass
finally:
return # warning
finally:
pass
def e():
if 1 == 2 - 1:
try:
def f():
try:
pass
finally:
return # warning
finally:
pass
def g():
try:
pass
finally:
def h():
return # no warning
e()
def i():
while True:
try:
pass
finally:
break # warning
def j():
while True:
break # no warning
def h():
while True:
try:
pass
finally:
continue # warning
def j():
while True:
continue # no warning
def k():
try:
pass
finally:
while True:
break # no warning
while True:
continue # no warning
while True:
return # warning
while True:
try:
pass
finally:
continue # warning
while True:
try:
pass
finally:
break # warning

40
resources/test/fixtures/B020.py vendored Normal file
View File

@@ -0,0 +1,40 @@
"""
Should emit:
B020 - on lines 8, 21, and 36
"""
items = [1, 2, 3]
for items in items:
print(items)
items = [1, 2, 3]
for item in items:
print(item)
values = {"secret": 123}
for key, value in values.items():
print(f"{key}, {value}")
for key, values in values.items():
print(f"{key}, {values}")
# Variables defined in a comprehension are local in scope
# to that comprehension and are therefore allowed.
for var in [var for var in range(10)]:
print(var)
for var in (var for var in range(10)):
print(var)
for k, v in {k: v for k, v in zip(range(10), range(10, 20))}.items():
print(k, v)
# However we still call out reassigning the iterable in the comprehension.
for vars in [i for i in vars]:
print(vars)
for var in sorted(range(10), key=lambda var: var.real):
print(var)

76
resources/test/fixtures/B021.py vendored Normal file
View File

@@ -0,0 +1,76 @@
f"""
Should emit:
B021 - on lines 14, 22, 30, 38, 46, 54, 62, 70, 73
"""
VARIABLE = "world"
def foo1():
"""hello world!"""
def foo2():
f"""hello {VARIABLE}!"""
class bar1:
"""hello world!"""
class bar2:
f"""hello {VARIABLE}!"""
def foo1():
"""hello world!"""
def foo2():
f"""hello {VARIABLE}!"""
class bar1:
"""hello world!"""
class bar2:
f"""hello {VARIABLE}!"""
def foo1():
"hello world!"
def foo2():
f"hello {VARIABLE}!"
class bar1:
"hello world!"
class bar2:
f"hello {VARIABLE}!"
def foo1():
"hello world!"
def foo2():
f"hello {VARIABLE}!"
class bar1:
"hello world!"
class bar2:
f"hello {VARIABLE}!"
def baz():
f"""I'm probably a docstring: {VARIABLE}!"""
print(f"""I'm a normal string""")
f"""Don't detect me!"""

23
resources/test/fixtures/B022.py vendored Normal file
View File

@@ -0,0 +1,23 @@
"""
Should emit:
B022 - on lines 8
"""
import contextlib
from contextlib import suppress
with contextlib.suppress():
raise ValueError
with suppress():
raise ValueError
with contextlib.suppress(ValueError):
raise ValueError
exceptions_to_suppress = []
if True:
exceptions_to_suppress.append(ValueError)
with contextlib.suppress(*exceptions_to_suppress):
raise

129
resources/test/fixtures/B024.py vendored Normal file
View File

@@ -0,0 +1,129 @@
"""
Should emit:
B024 - on lines 17, 34, 52, 58, 69, 74, 79, 84, 89
"""
import abc
import abc as notabc
from abc import ABC, ABCMeta
from abc import abstractmethod
from abc import abstractmethod as abstract
from abc import abstractmethod as abstractaoeuaoeuaoeu
from abc import abstractmethod as notabstract
import foo
class Base_1(ABC): # error
def method(self):
foo()
class Base_2(ABC):
@abstractmethod
def method(self):
foo()
class Base_3(ABC):
@abc.abstractmethod
def method(self):
foo()
class Base_4(ABC):
@notabc.abstractmethod
def method(self):
foo()
class Base_5(ABC):
@abstract
def method(self):
foo()
class Base_6(ABC):
@abstractaoeuaoeuaoeu
def method(self):
foo()
class Base_7(ABC): # error
@notabstract
def method(self):
foo()
class MetaBase_1(metaclass=ABCMeta): # error
def method(self):
foo()
class MetaBase_2(metaclass=ABCMeta):
@abstractmethod
def method(self):
foo()
class abc_Base_1(abc.ABC): # error
def method(self):
foo()
class abc_Base_2(metaclass=abc.ABCMeta): # error
def method(self):
foo()
class notabc_Base_1(notabc.ABC): # error
def method(self):
foo()
class multi_super_1(notabc.ABC, abc.ABCMeta): # safe
def method(self):
foo()
class multi_super_2(notabc.ABC, metaclass=abc.ABCMeta): # safe
def method(self):
foo()
class non_keyword_abcmeta_1(ABCMeta): # safe
def method(self):
foo()
class non_keyword_abcmeta_2(abc.ABCMeta): # safe
def method(self):
foo()
# very invalid code, but that's up to mypy et al to check
class keyword_abc_1(metaclass=ABC): # safe
def method(self):
foo()
class keyword_abc_2(metaclass=abc.ABC): # safe
def method(self):
foo()
class abc_set_class_variable_1(ABC): # safe
foo: int
class abc_set_class_variable_2(ABC): # safe
foo = 2
class abc_set_class_variable_3(ABC): # safe
foo: int = 2
# this doesn't actually declare a class variable, it's just an expression
class abc_set_class_variable_4(ABC): # error
foo

88
resources/test/fixtures/B027.py vendored Normal file
View File

@@ -0,0 +1,88 @@
"""
Should emit:
B027 - on lines 12, 15, 18, 22, 30
"""
import abc
from abc import ABC
from abc import abstractmethod
from abc import abstractmethod as notabstract
class AbstractClass(ABC):
def empty_1(self): # error
...
def empty_2(self): # error
pass
def empty_3(self): # error
"""docstring"""
...
def empty_4(self): # error
"""multiple ellipsis/pass"""
...
pass
...
pass
@notabstract
def abstract_0(self):
...
@abstractmethod
def abstract_1(self):
...
@abstractmethod
def abstract_2(self):
pass
@abc.abstractmethod
def abstract_3(self):
...
def body_1(self):
print("foo")
...
def body_2(self):
self.body_1()
class NonAbstractClass:
def empty_1(self): # safe
...
def empty_2(self): # safe
pass
# ignore @overload, fixes issue #304
# ignore overload with other imports, fixes #308
import typing
import typing as t
import typing as anything
from typing import Union, overload
class AbstractClass(ABC):
@overload
def empty_1(self, foo: str):
...
@typing.overload
def empty_1(self, foo: int):
...
@t.overload
def empty_1(self, foo: list):
...
@anything.overload
def empty_1(self, foo: float):
...
@abstractmethod
def empty_1(self, foo: Union[str, int, list, float]):
...

20
resources/test/fixtures/F401_6.py vendored Normal file
View File

@@ -0,0 +1,20 @@
"""Test: explicit re-export."""
# OK
from .applications import FastAPI as FastAPI
# F401 `background.BackgroundTasks` imported but unused
from .background import BackgroundTasks
# F401 `datastructures.UploadFile` imported but unused
from .datastructures import UploadFile as FileUpload
# OK
import applications as applications
# F401 `background` imported but unused
import background
# F401 `datastructures` imported but unused
import datastructures as structures

24
resources/test/fixtures/F821_4.py vendored Normal file
View File

@@ -0,0 +1,24 @@
"""Test: import alias tracking."""
from typing import List
_ = List["Model"]
from typing import List as IList
_ = IList["Model"]
from collections.abc import ItemsView
_ = ItemsView["Model"]
import collections.abc
_ = collections.abc.ItemsView["Model"]
from collections import abc
_ = abc.ItemsView["Model"]

14
resources/test/fixtures/F821_5.py vendored Normal file
View File

@@ -0,0 +1,14 @@
"""Test: inner class annotation."""
class RandomClass:
def random_func(self) -> "InnerClass":
pass
class OuterClass:
class InnerClass:
pass
def failing_func(self) -> "InnerClass":
return self.InnerClass()

View File

@@ -1,5 +1,11 @@
import collections
from collections import namedtuple
def f():
lower = 0
Camel = 0
CONSTANT = 0
_ = 0
MyObj1 = collections.namedtuple("MyObj1", ["a", "b"])
MyObj2 = namedtuple("MyObj12", ["a", "b"])

View File

@@ -1,6 +1,12 @@
import collections
from collections import namedtuple
class C:
lower = 0
CONSTANT = 0
mixedCase = 0
_mixedCase = 0
mixed_Case = 0
myObj1 = collections.namedtuple("MyObj1", ["a", "b"])
myObj2 = namedtuple("MyObj2", ["a", "b"])

View File

@@ -1,5 +1,10 @@
import collections
from collections import namedtuple
lower = 0
CONSTANT = 0
mixedCase = 0
_mixedCase = 0
mixed_Case = 0
myObj1 = collections.namedtuple("MyObj1", ["a", "b"])
myObj2 = namedtuple("MyObj2", ["a", "b"])

11
resources/test/fixtures/S101.py vendored Normal file
View File

@@ -0,0 +1,11 @@
# Error
assert True
def fn():
x = 1
# Error
assert x == 1
# Error
assert x == 2

5
resources/test/fixtures/S102.py vendored Normal file
View File

@@ -0,0 +1,5 @@
def fn():
# Error
exec('x = 2')
exec('y = 3')

19
resources/test/fixtures/S104.py vendored Normal file
View File

@@ -0,0 +1,19 @@
def func(address):
print(address)
# OK
"OK"
# Error
"0.0.0.0"
'0.0.0.0'
# Error
func("0.0.0.0")
def my_func():
x = "0.0.0.0"
print(x)

53
resources/test/fixtures/S105.py vendored Normal file
View File

@@ -0,0 +1,53 @@
d = {}
# OK
safe = "s3cr3t"
password = True
password = safe
password is True
password == 1
d["safe"] = "s3cr3t"
# Errors
password = "s3cr3t"
_pass = "s3cr3t"
passwd = "s3cr3t"
pwd = "s3cr3t"
secret = "s3cr3t"
token = "s3cr3t"
secrete = "s3cr3t"
safe = password = "s3cr3t"
password = safe = "s3cr3t"
d["password"] = "s3cr3t"
d["pass"] = "s3cr3t"
d["passwd"] = "s3cr3t"
d["pwd"] = "s3cr3t"
d["secret"] = "s3cr3t"
d["token"] = "s3cr3t"
d["secrete"] = "s3cr3t"
safe = d["password"] = "s3cr3t"
d["password"] = safe = "s3cr3t"
class MyClass:
password = "s3cr3t"
safe = password
MyClass.password = "s3cr3t"
MyClass._pass = "s3cr3t"
MyClass.passwd = "s3cr3t"
MyClass.pwd = "s3cr3t"
MyClass.secret = "s3cr3t"
MyClass.token = "s3cr3t"
MyClass.secrete = "s3cr3t"
password == "s3cr3t"
_pass == "s3cr3t"
passwd == "s3cr3t"
pwd == "s3cr3t"
secret == "s3cr3t"
token == "s3cr3t"
secrete == "s3cr3t"
password == safe == "s3cr3t"

13
resources/test/fixtures/S106.py vendored Normal file
View File

@@ -0,0 +1,13 @@
def func(pos, password):
pass
string = "Hello World"
# OK
func("s3cr3t")
func(1, password=string)
func(pos="s3cr3t", password=string)
# Error
func(1, password="s3cr3t")

30
resources/test/fixtures/S107.py vendored Normal file
View File

@@ -0,0 +1,30 @@
def ok(first, default="default"):
pass
def default(first, password="default"):
pass
def ok_posonly(first, /, pos, default="posonly"):
pass
def default_posonly(first, /, pos, password="posonly"):
pass
def ok_kwonly(first, *, default="kwonly"):
pass
def default_kwonly(first, *, password="kwonly"):
pass
def ok_all(first, /, pos, default="posonly", *, kwonly="kwonly"):
pass
def default_all(first, /, pos, secret="posonly", *, password="kwonly"):
pass

View File

@@ -1,3 +1,10 @@
import typing
def f(x: typing.List[str]) -> None:
...
from typing import List
@@ -5,8 +12,15 @@ def f(x: List[str]) -> None:
...
import typing
import typing as t
def f(x: typing.List[str]) -> None:
def f(x: t.List[str]) -> None:
...
from typing import List as IList
def f(x: IList[str]) -> None:
...

View File

@@ -5,9 +5,8 @@ print(sys.version)
print(sys.version[:3])
print(version[:3])
# ignore from imports with aliases, patches welcome
print(v[:3])
# the tool is timid and only flags certain numeric slices
i = 3
print(sys.version[:i])

View File

@@ -0,0 +1,26 @@
from a import b
from a import BAD as DEF
from a import B
from a import Boo as DEF
from a import B as Abc
from a import B as A
from a import B as DEF
from a import b as a
from a import b as x
from a import b as c
from b import c
from a import b as d
from a import b as y
from b import C
from b import c as d
import A
import a
import b
import B
import x as y
import x as A
import x as Y
import x
import x as a

View File

@@ -7,6 +7,9 @@ extend-exclude = [
]
per-file-ignores = { "__init__.py" = ["F401"] }
[tool.ruff.flake8-bugbear]
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
[tool.ruff.flake8-quotes]
inline-quotes = "single"
multiline-quotes = "double"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.114"
version = "0.0.121"
edition = "2021"
[dependencies]

View File

@@ -1,16 +1,16 @@
use std::collections::BTreeSet;
use fnv::{FnvHashMap, FnvHashSet};
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, StmtKind};
fn compose_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
#[inline(always)]
fn collect_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
match &expr.node {
ExprKind::Call { func, .. } => {
compose_call_path_inner(func, parts);
collect_call_path_inner(func, parts);
}
ExprKind::Attribute { value, attr, .. } => {
compose_call_path_inner(value, parts);
collect_call_path_inner(value, parts);
parts.push(attr);
}
ExprKind::Name { id, .. } => {
@@ -20,9 +20,10 @@ fn compose_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
}
}
/// Convert an `Expr` to its call path (like `List`, or `typing.List`).
#[inline(always)]
pub fn compose_call_path(expr: &Expr) -> Option<String> {
let mut segments = vec![];
compose_call_path_inner(expr, &mut segments);
let segments = collect_call_paths(expr);
if segments.is_empty() {
None
} else {
@@ -30,6 +31,34 @@ pub fn compose_call_path(expr: &Expr) -> Option<String> {
}
}
/// Convert an `Expr` to its call path segments (like ["typing", "List"]).
#[inline(always)]
pub fn collect_call_paths(expr: &Expr) -> Vec<&str> {
let mut segments = vec![];
collect_call_path_inner(expr, &mut segments);
segments
}
/// Rewrite any import aliases on a call path.
pub fn dealias_call_path<'a>(
call_path: Vec<&'a str>,
import_aliases: &FnvHashMap<&str, &'a str>,
) -> Vec<&'a str> {
if let Some(head) = call_path.first() {
if let Some(origin) = import_aliases.get(head) {
let tail = &call_path[1..];
let mut call_path: Vec<&str> = vec![];
call_path.extend(origin.split('.'));
call_path.extend(tail);
call_path
} else {
call_path
}
} else {
call_path
}
}
/// Return `true` if the `Expr` is a name or attribute reference to `${target}`.
pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
match &expr.node {
@@ -43,24 +72,87 @@ pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
///
/// Useful for, e.g., ensuring that a `Union` reference represents
/// `typing.Union`.
pub fn match_name_or_attr_from_module(
pub fn match_module_member(
expr: &Expr,
target: &str,
module: &str,
imports: Option<&BTreeSet<&str>>,
member: &str,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
match &expr.node {
ExprKind::Attribute { value, attr, .. } => match &value.node {
ExprKind::Name { id, .. } => id == module && target == attr,
_ => false,
},
ExprKind::Name { id, .. } => {
target == id
&& imports
.map(|imports| imports.contains(&id.as_str()))
.unwrap_or_default()
match_call_path(
&dealias_call_path(collect_call_paths(expr), import_aliases),
module,
member,
from_imports,
)
}
/// Return `true` if the `call_path` is a reference to `${module}.${target}`.
///
/// Optimized version of `match_module_member` for pre-computed call paths.
pub fn match_call_path(
call_path: &[&str],
module: &str,
member: &str,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> bool {
// If we have no segments, we can't ever match.
let num_segments = call_path.len();
if num_segments == 0 {
return false;
}
// If the last segment doesn't match the member, we can't ever match.
if call_path[num_segments - 1] != member {
return false;
}
// We now only need the module path, so throw out the member name.
let call_path = &call_path[..num_segments - 1];
let num_segments = call_path.len();
// Case (1): It's a builtin (like `list`).
// Case (2a): We imported from the parent (`from typing.re import Match`,
// `Match`).
// Case (2b): We imported star from the parent (`from typing.re import *`,
// `Match`).
if num_segments == 0 {
module.is_empty()
|| from_imports
.get(module)
.map(|imports| imports.contains(member) || imports.contains("*"))
.unwrap_or(false)
} else {
let components: Vec<&str> = module.split('.').collect();
// Case (3a): it's a fully qualified call path (`import typing`,
// `typing.re.Match`). Case (3b): it's a fully qualified call path (`import
// typing.re`, `typing.re.Match`).
if components == call_path {
return true;
}
_ => false,
// Case (4): We imported from the grandparent (`from typing import re`,
// `re.Match`)
let num_matches = (0..components.len())
.take(num_segments)
.take_while(|i| components[components.len() - 1 - i] == call_path[num_segments - 1 - i])
.count();
if num_matches > 0 {
let cut = components.len() - num_matches;
// TODO(charlie): Rewrite to avoid this allocation.
let module = components[..cut].join(".");
let member = components[cut];
if from_imports
.get(&module.as_str())
.map(|imports| imports.contains(member))
.unwrap_or(false)
{
return true;
}
}
false
}
}
@@ -97,7 +189,7 @@ pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
}
/// Extract the names of all handled exceptions.
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<String> {
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<Vec<&str>> {
let mut handler_names = vec![];
for handler in handlers {
match &handler.node {
@@ -105,12 +197,16 @@ pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<String> {
if let Some(type_) = type_ {
if let ExprKind::Tuple { elts, .. } = &type_.node {
for type_ in elts {
if let Some(name) = compose_call_path(type_) {
handler_names.push(name);
let call_path = collect_call_paths(type_);
if !call_path.is_empty() {
handler_names.push(call_path);
}
}
} else if let Some(name) = compose_call_path(type_) {
handler_names.push(name);
} else {
let call_path = collect_call_paths(type_);
if !call_path.is_empty() {
handler_names.push(call_path);
}
}
}
}
@@ -141,3 +237,155 @@ pub fn to_absolute(relative: &Location, base: &Location) -> Location {
Location::new(relative.row() + base.row() - 1, relative.column())
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_parser::parser;
use crate::ast::helpers::match_module_member;
#[test]
fn builtin() -> Result<()> {
let expr = parser::parse_expression("list", "<filename>")?;
assert!(match_module_member(
&expr,
"",
"list",
&FnvHashMap::default(),
&FnvHashMap::default(),
));
Ok(())
}
#[test]
fn fully_qualified() -> Result<()> {
let expr = parser::parse_expression("typing.re.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::default(),
&FnvHashMap::default(),
));
Ok(())
}
#[test]
fn unimported() -> Result<()> {
let expr = parser::parse_expression("Match", "<filename>")?;
assert!(!match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::default(),
&FnvHashMap::default(),
));
let expr = parser::parse_expression("re.Match", "<filename>")?;
assert!(!match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::default(),
&FnvHashMap::default(),
));
Ok(())
}
#[test]
fn from_star() -> Result<()> {
let expr = parser::parse_expression("Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["*"]))]),
&FnvHashMap::default()
));
Ok(())
}
#[test]
fn from_parent() -> Result<()> {
let expr = parser::parse_expression("Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["Match"]))]),
&FnvHashMap::default()
));
Ok(())
}
#[test]
fn from_grandparent() -> Result<()> {
let expr = parser::parse_expression("re.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::from_iter([("typing", FnvHashSet::from_iter(["re"]))]),
&FnvHashMap::default()
));
let expr = parser::parse_expression("match.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.match",
"Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["match"]))]),
&FnvHashMap::default()
));
let expr = parser::parse_expression("re.match.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.match",
"Match",
&FnvHashMap::from_iter([("typing", FnvHashSet::from_iter(["re"]))]),
&FnvHashMap::default()
));
Ok(())
}
#[test]
fn from_alias() -> Result<()> {
let expr = parser::parse_expression("IMatch", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["Match"]))]),
&FnvHashMap::from_iter([("IMatch", "Match")]),
));
Ok(())
}
#[test]
fn from_aliased_parent() -> Result<()> {
let expr = parser::parse_expression("t.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::default(),
&FnvHashMap::from_iter([("t", "typing.re")]),
));
Ok(())
}
#[test]
fn from_aliased_grandparent() -> Result<()> {
let expr = parser::parse_expression("t.re.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re",
"Match",
&FnvHashMap::default(),
&FnvHashMap::from_iter([("t", "typing")]),
));
Ok(())
}
}

View File

@@ -67,9 +67,8 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
}
/// Check if a node is parent of a conditional branch.
pub fn on_conditional_branch(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
for index in parent_stack.iter().rev() {
let parent = parents[*index];
pub fn on_conditional_branch<'a>(parents: &mut impl Iterator<Item = &'a Stmt>) -> bool {
parents.any(|parent| {
if matches!(parent.node, StmtKind::If { .. } | StmtKind::While { .. }) {
return true;
}
@@ -78,24 +77,18 @@ pub fn on_conditional_branch(parent_stack: &[usize], parents: &[&Stmt]) -> bool
return true;
}
}
}
false
false
})
}
/// Check if a node is in a nested block.
pub fn in_nested_block(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
for index in parent_stack.iter().rev() {
let parent = parents[*index];
if matches!(
pub fn in_nested_block<'a>(parents: &mut impl Iterator<Item = &'a Stmt>) -> bool {
parents.any(|parent| {
matches!(
parent.node,
StmtKind::Try { .. } | StmtKind::If { .. } | StmtKind::With { .. }
) {
return true;
}
}
false
)
})
}
/// Check if a node represents an unpacking assignment.

View File

@@ -1,6 +1,6 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use fnv::FnvHashMap;
use rustpython_ast::{Expr, Keyword};
use rustpython_parser::ast::{Located, Location};
@@ -54,7 +54,7 @@ pub struct Scope<'a> {
pub id: usize,
pub kind: ScopeKind<'a>,
pub import_starred: bool,
pub values: BTreeMap<String, Binding>,
pub values: FnvHashMap<String, Binding>,
}
impl<'a> Scope<'a> {
@@ -63,7 +63,7 @@ impl<'a> Scope<'a> {
id: id(),
kind,
import_starred: false,
values: BTreeMap::new(),
values: FnvHashMap::default(),
}
}
}

View File

@@ -61,7 +61,7 @@ fn apply_fixes<'a>(
) -> Cow<'a, str> {
let mut output = RopeBuilder::new();
let mut last_pos: Location = Location::new(1, 0);
let mut applied: BTreeSet<&Patch> = BTreeSet::new();
let mut applied: BTreeSet<&Patch> = BTreeSet::default();
for fix in fixes.sorted_by_key(|fix| fix.patch.location) {
// If we already applied an identical fix as part of another correction, skip

View File

@@ -23,7 +23,7 @@ use crate::autofix::fixer;
use crate::message::Message;
use crate::settings::Settings;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Serialize, Deserialize)]
struct CacheMetadata {
@@ -89,7 +89,7 @@ fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String
format!(
"{}@{}@{}",
path.absolutize().unwrap().to_string_lossy(),
VERSION,
CARGO_PKG_VERSION,
hasher.finish()
)
}

View File

@@ -1,9 +1,11 @@
//! Lint rules based on AST traversal.
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeMap;
use std::ops::Deref;
use std::path::Path;
use fnv::{FnvHashMap, FnvHashSet};
use itertools::Itertools;
use log::error;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
@@ -11,7 +13,9 @@ use rustpython_parser::ast::{
};
use rustpython_parser::parser;
use crate::ast::helpers::{extract_handler_names, match_name_or_attr_from_module};
use crate::ast::helpers::{
collect_call_paths, dealias_call_path, extract_handler_names, match_call_path,
};
use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
@@ -32,25 +36,11 @@ use crate::settings::Settings;
use crate::source_code_locator::SourceCodeLocator;
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bugbear, flake8_builtins,
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins,
flake8_comprehensions, flake8_print, pep8_naming, pycodestyle, pydocstyle, pyflakes, pyupgrade,
};
const GLOBAL_SCOPE_INDEX: usize = 0;
const TRACK_FROM_IMPORTS: [&str; 12] = [
"collections",
"collections.abc",
"contextlib",
"functools",
"re",
"six",
"sys",
"typing",
"typing.io",
"typing.re",
"typing_extensions",
"weakref",
];
pub struct Checker<'a> {
// Input data.
@@ -64,9 +54,10 @@ pub struct Checker<'a> {
definitions: Vec<(Definition<'a>, Visibility)>,
// Edit tracking.
// TODO(charlie): Instead of exposing deletions, wrap in a public API.
pub(crate) deletions: BTreeSet<usize>,
pub(crate) deletions: FnvHashSet<usize>,
// Import tracking.
pub(crate) from_imports: BTreeMap<&'a str, BTreeSet<&'a str>>,
pub(crate) from_imports: FnvHashMap<&'a str, FnvHashSet<&'a str>>,
pub(crate) import_aliases: FnvHashMap<&'a str, &'a str>,
// Retain all scopes and parent nodes, along with a stack of indexes to track which are active
// at various points in time.
pub(crate) parents: Vec<&'a Stmt>,
@@ -74,7 +65,7 @@ pub struct Checker<'a> {
scopes: Vec<Scope<'a>>,
scope_stack: Vec<usize>,
dead_scopes: Vec<usize>,
deferred_string_annotations: Vec<(Range, &'a str)>,
deferred_string_annotations: Vec<(Range, &'a str, Vec<usize>, Vec<usize>)>,
deferred_annotations: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
deferred_functions: Vec<(&'a Stmt, Vec<usize>, Vec<usize>, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
@@ -88,7 +79,7 @@ pub struct Checker<'a> {
seen_import_boundary: bool,
futures_allowed: bool,
annotations_future_enabled: bool,
except_handlers: Vec<Vec<String>>,
except_handlers: Vec<Vec<Vec<&'a str>>>,
}
impl<'a> Checker<'a> {
@@ -107,6 +98,7 @@ impl<'a> Checker<'a> {
definitions: Default::default(),
deletions: Default::default(),
from_imports: Default::default(),
import_aliases: Default::default(),
parents: Default::default(),
parent_stack: Default::default(),
scopes: Default::default(),
@@ -167,15 +159,16 @@ impl<'a> Checker<'a> {
}
/// Return `true` if the `Expr` is a reference to `typing.${target}`.
pub fn match_typing_module(&self, expr: &Expr, target: &str) -> bool {
match_name_or_attr_from_module(expr, target, "typing", self.from_imports.get("typing"))
pub fn match_typing_expr(&self, expr: &Expr, target: &str) -> bool {
let call_path = dealias_call_path(collect_call_paths(expr), &self.import_aliases);
self.match_typing_call_path(&call_path, target)
}
/// Return `true` if the call path is a reference to `typing.${target}`.
pub fn match_typing_call_path(&self, call_path: &[&str], target: &str) -> bool {
match_call_path(call_path, "typing", target, &self.from_imports)
|| (typing::in_extensions(target)
&& match_name_or_attr_from_module(
expr,
target,
"typing_extensions",
self.from_imports.get("typing_extensions"),
))
&& match_call_path(call_path, "typing_extensions", target, &self.from_imports))
}
}
@@ -205,7 +198,13 @@ where
self.futures_allowed = false;
if !self.seen_import_boundary
&& !helpers::is_assignment_to_a_dunder(node)
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
&& !operations::in_nested_block(
&mut self
.parent_stack
.iter()
.rev()
.map(|index| self.parents[*index]),
)
{
self.seen_import_boundary = true;
}
@@ -350,6 +349,12 @@ where
flake8_bugbear::plugins::cached_instance_method(self, decorator_list);
}
if self.settings.enabled.contains(&CheckCode::S107) {
self.add_checks(
flake8_bandit::plugins::hardcoded_password_default(args).into_iter(),
);
}
self.check_builtin_shadowing(name, Range::from_located(stmt), true);
// Visit the decorators and arguments, but avoid the body, which will be
@@ -403,14 +408,14 @@ where
StmtKind::Return { .. } => {
if self.settings.enabled.contains(&CheckCode::F706) {
if let Some(index) = self.scope_stack.last().cloned() {
match self.scopes[index].kind {
ScopeKind::Class(_) | ScopeKind::Module => {
self.add_check(Check::new(
CheckKind::ReturnOutsideFunction,
Range::from_located(stmt),
));
}
_ => {}
if matches!(
self.scopes[index].kind,
ScopeKind::Class(_) | ScopeKind::Module
) {
self.add_check(Check::new(
CheckKind::ReturnOutsideFunction,
Range::from_located(stmt),
));
}
}
}
@@ -454,6 +459,14 @@ where
flake8_bugbear::plugins::useless_expression(self, body);
}
if self.settings.enabled.contains(&CheckCode::B024)
|| self.settings.enabled.contains(&CheckCode::B027)
{
flake8_bugbear::plugins::abstract_base_class(
self, stmt, name, bases, keywords, body,
);
}
self.check_builtin_shadowing(name, Range::from_located(stmt), false);
for expr in bases {
@@ -518,13 +531,38 @@ where
full_name.to_string(),
self.binding_context(),
),
used: None,
// Treat explicit re-export as usage (e.g., `import applications
// as applications`).
used: if alias
.node
.asname
.as_ref()
.map(|asname| asname == &alias.node.name)
.unwrap_or(false)
{
Some((
self.scopes[*(self
.scope_stack
.last()
.expect("No current scope found."))]
.id,
Range::from_located(stmt),
))
} else {
None
},
range: Range::from_located(stmt),
},
)
}
if let Some(asname) = &alias.node.asname {
for alias in names {
if let Some(asname) = &alias.node.asname {
self.import_aliases.insert(asname, &alias.node.name);
}
}
let name = alias.node.name.split('.').last().unwrap();
if self.settings.enabled.contains(&CheckCode::N811) {
if let Some(check) =
@@ -583,16 +621,19 @@ where
// references like `from typing import Union`.
if level.map(|level| level == 0).unwrap_or(true) {
if let Some(module) = module {
if TRACK_FROM_IMPORTS.contains(&module.as_str()) {
self.from_imports
.entry(module)
.or_insert_with(BTreeSet::new)
.extend(
names
.iter()
.filter(|alias| alias.node.asname.is_none())
.map(|alias| alias.node.name.as_str()),
)
self.from_imports
.entry(module)
.or_insert_with(FnvHashSet::default)
.extend(
names
.iter()
.filter(|alias| alias.node.asname.is_none())
.map(|alias| alias.node.name.as_str()),
)
}
for alias in names {
if let Some(asname) = &alias.node.asname {
self.import_aliases.insert(asname, &alias.node.name);
}
}
}
@@ -619,6 +660,7 @@ where
name.to_string(),
Binding {
kind: BindingKind::FutureImportation,
// Always mark `__future__` imports as used.
used: Some((
self.scopes[*(self
.scope_stack
@@ -711,7 +753,26 @@ where
full_name,
self.binding_context(),
),
used: None,
// Treat explicit re-export as usage (e.g., `from .applications
// import FastAPI as FastAPI`).
used: if alias
.node
.asname
.as_ref()
.map(|asname| asname == &alias.node.name)
.unwrap_or(false)
{
Some((
self.scopes[*(self
.scope_stack
.last()
.expect("No current scope found."))]
.id,
Range::from_located(stmt),
))
} else {
None
},
range: Range::from_located(stmt),
},
)
@@ -803,16 +864,24 @@ where
if self.settings.enabled.contains(&CheckCode::B011) {
flake8_bugbear::plugins::assert_false(self, stmt, test, msg);
}
if self.settings.enabled.contains(&CheckCode::S101) {
self.add_check(flake8_bandit::plugins::assert_used(stmt));
}
}
StmtKind::With { items, .. } | StmtKind::AsyncWith { items, .. } => {
if self.settings.enabled.contains(&CheckCode::B017) {
flake8_bugbear::plugins::assert_raises_exception(self, stmt, items);
}
}
StmtKind::For { target, body, .. } => {
StmtKind::For {
target, body, iter, ..
} => {
if self.settings.enabled.contains(&CheckCode::B007) {
flake8_bugbear::plugins::unused_loop_control_variable(self, target, body);
}
if self.settings.enabled.contains(&CheckCode::B020) {
flake8_bugbear::plugins::loop_variable_overrides_iterator(self, target, iter);
}
}
StmtKind::Try { handlers, .. } => {
if self.settings.enabled.contains(&CheckCode::F707) {
@@ -843,6 +912,13 @@ where
if self.settings.enabled.contains(&CheckCode::B003) {
flake8_bugbear::plugins::assignment_to_os_environ(self, targets);
}
if self.settings.enabled.contains(&CheckCode::S105) {
if let Some(check) =
flake8_bandit::plugins::assign_hardcoded_password_string(value, targets)
{
self.add_check(check);
}
}
}
StmtKind::AnnAssign { value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
@@ -869,6 +945,9 @@ where
let prev_visible_scope = self.visible_scope.clone();
match &stmt.node {
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
if self.settings.enabled.contains(&CheckCode::B021) {
flake8_bugbear::plugins::f_string_docstring(self, body);
}
let definition = docstrings::extraction::extract(
&self.visible_scope,
stmt,
@@ -888,6 +967,9 @@ where
));
}
StmtKind::ClassDef { body, .. } => {
if self.settings.enabled.contains(&CheckCode::B021) {
flake8_bugbear::plugins::f_string_docstring(self, body);
}
let definition = docstrings::extraction::extract(
&self.visible_scope,
stmt,
@@ -910,6 +992,9 @@ where
finalbody,
} => {
self.except_handlers.push(extract_handler_names(handlers));
if self.settings.enabled.contains(&CheckCode::B012) {
flake8_bugbear::plugins::jump_statement_in_finally(self, finalbody);
}
for stmt in body {
self.visit_stmt(stmt);
}
@@ -962,8 +1047,12 @@ where
..
} = &expr.node
{
self.deferred_string_annotations
.push((Range::from_located(expr), value));
self.deferred_string_annotations.push((
Range::from_located(expr),
value,
self.scope_stack.clone(),
self.parent_stack.clone(),
));
} else {
self.deferred_annotations.push((
expr,
@@ -979,12 +1068,12 @@ where
ExprKind::Subscript { value, slice, .. } => {
// Ex) typing.List[...]
if self.settings.enabled.contains(&CheckCode::U007)
&& self.settings.target_version >= PythonVersion::Py39
&& self.settings.target_version >= PythonVersion::Py310
{
pyupgrade::plugins::use_pep604_annotation(self, expr, value, slice);
}
if self.match_typing_module(value, "Literal") {
if self.match_typing_expr(value, "Literal") {
self.in_literal = true;
}
@@ -1018,7 +1107,11 @@ where
// Ex) List[...]
if self.settings.enabled.contains(&CheckCode::U006)
&& self.settings.target_version >= PythonVersion::Py39
&& typing::is_pep585_builtin(expr, self.from_imports.get("typing"))
&& typing::is_pep585_builtin(
expr,
&self.from_imports,
&self.import_aliases,
)
{
pyupgrade::plugins::use_pep585_annotation(self, expr, id);
}
@@ -1050,7 +1143,7 @@ where
// Ex) typing.List[...]
if self.settings.enabled.contains(&CheckCode::U006)
&& self.settings.target_version >= PythonVersion::Py39
&& typing::is_pep585_builtin(expr, self.from_imports.get("typing"))
&& typing::is_pep585_builtin(expr, &self.from_imports, &self.import_aliases)
{
pyupgrade::plugins::use_pep585_annotation(self, expr, attr);
}
@@ -1103,11 +1196,24 @@ where
flake8_bugbear::plugins::setattr_with_constant(self, expr, func, args);
}
}
if self.settings.enabled.contains(&CheckCode::B022) {
flake8_bugbear::plugins::useless_contextlib_suppress(self, expr, args);
}
if self.settings.enabled.contains(&CheckCode::B026) {
flake8_bugbear::plugins::star_arg_unpacking_after_keyword_arg(
self, args, keywords,
);
}
if self.settings.enabled.contains(&CheckCode::S102) {
if let Some(check) = flake8_bandit::plugins::exec_used(expr, func) {
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::S106) {
self.add_checks(
flake8_bandit::plugins::hardcoded_password_func_arg(keywords).into_iter(),
);
}
// flake8-comprehensions
if self.settings.enabled.contains(&CheckCode::C400) {
@@ -1456,14 +1562,36 @@ where
{
flake8_2020::plugins::compare(self, left, ops, comparators);
}
if self.settings.enabled.contains(&CheckCode::S105) {
self.add_checks(
flake8_bandit::plugins::compare_to_hardcoded_password_string(
left,
comparators,
)
.into_iter(),
);
}
}
ExprKind::Constant {
value: Constant::Str(value),
..
} => {
if self.in_annotation && !self.in_literal {
self.deferred_string_annotations
.push((Range::from_located(expr), value));
self.deferred_string_annotations.push((
Range::from_located(expr),
value,
self.scope_stack.clone(),
self.parent_stack.clone(),
));
}
if self.settings.enabled.contains(&CheckCode::S104) {
if let Some(check) = flake8_bandit::plugins::hardcoded_bind_all_interfaces(
value,
&Range::from_located(expr),
) {
self.add_check(check);
}
}
}
ExprKind::Lambda { args, .. } => {
@@ -1536,12 +1664,13 @@ where
args,
keywords,
} => {
if self.match_typing_module(func, "ForwardRef") {
let call_path = dealias_call_path(collect_call_paths(func), &self.import_aliases);
if self.match_typing_call_path(&call_path, "ForwardRef") {
self.visit_expr(func);
for expr in args {
self.visit_annotation(expr);
}
} else if self.match_typing_module(func, "cast") {
} else if self.match_typing_call_path(&call_path, "cast") {
self.visit_expr(func);
if !args.is_empty() {
self.visit_annotation(&args[0]);
@@ -1549,12 +1678,12 @@ where
for expr in args.iter().skip(1) {
self.visit_expr(expr);
}
} else if self.match_typing_module(func, "NewType") {
} else if self.match_typing_call_path(&call_path, "NewType") {
self.visit_expr(func);
for expr in args.iter().skip(1) {
self.visit_annotation(expr);
}
} else if self.match_typing_module(func, "TypeVar") {
} else if self.match_typing_call_path(&call_path, "TypeVar") {
self.visit_expr(func);
for expr in args.iter().skip(1) {
self.visit_annotation(expr);
@@ -1571,7 +1700,7 @@ where
}
}
}
} else if self.match_typing_module(func, "NamedTuple") {
} else if self.match_typing_call_path(&call_path, "NamedTuple") {
self.visit_expr(func);
// Ex) NamedTuple("a", [("a", int)])
@@ -1603,7 +1732,7 @@ where
let KeywordData { value, .. } = &keyword.node;
self.visit_annotation(value);
}
} else if self.match_typing_module(func, "TypedDict") {
} else if self.match_typing_call_path(&call_path, "TypedDict") {
self.visit_expr(func);
// Ex) TypedDict("a", {"a": int})
@@ -1642,7 +1771,11 @@ where
visitor::walk_expr(self, expr);
} else {
self.in_subscript = true;
match typing::match_annotated_subscript(value, &self.from_imports) {
match typing::match_annotated_subscript(
value,
&self.from_imports,
&self.import_aliases,
) {
Some(subscript) => {
match subscript {
// Ex) Optional[int]
@@ -2008,6 +2141,7 @@ impl<'a> Checker<'a> {
let mut import_starred = false;
for scope_index in self.scope_stack.iter().rev() {
let scope = &mut self.scopes[*scope_index];
if matches!(scope.kind, ScopeKind::Class(_)) {
if id == "__class__" {
return;
@@ -2054,7 +2188,10 @@ impl<'a> Checker<'a> {
// Avoid flagging if NameError is handled.
if let Some(handler_names) = self.except_handlers.last() {
if handler_names.contains(&"NameError".to_string()) {
if handler_names
.iter()
.any(|call_path| call_path.len() == 1 && call_path[0] == "NameError")
{
return;
}
}
@@ -2081,32 +2218,22 @@ impl<'a> Checker<'a> {
}
if self.settings.enabled.contains(&CheckCode::N806) {
let current =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if let Some(check) =
pep8_naming::checks::non_lowercase_variable_in_function(current, expr, id)
{
self.add_check(check);
if matches!(self.current_scope().kind, ScopeKind::Function(..)) {
pep8_naming::plugins::non_lowercase_variable_in_function(self, expr, parent, id)
}
}
if self.settings.enabled.contains(&CheckCode::N815) {
let current =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if let Some(check) =
pep8_naming::checks::mixed_case_variable_in_class_scope(current, expr, id)
{
self.add_check(check);
if matches!(self.current_scope().kind, ScopeKind::Class(..)) {
pep8_naming::plugins::mixed_case_variable_in_class_scope(self, expr, parent, id)
}
}
if self.settings.enabled.contains(&CheckCode::N816) {
let current =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if let Some(check) =
pep8_naming::checks::mixed_case_variable_in_global_scope(current, expr, id)
{
self.add_check(check);
if matches!(self.current_scope().kind, ScopeKind::Module) {
pep8_naming::plugins::mixed_case_variable_in_global_scope(
self, expr, parent, id,
)
}
}
@@ -2185,7 +2312,13 @@ impl<'a> Checker<'a> {
fn handle_node_delete(&mut self, expr: &Expr) {
if let ExprKind::Name { id, .. } = &expr.node {
if operations::on_conditional_branch(&self.parent_stack, &self.parents) {
if operations::on_conditional_branch(
&mut self
.parent_stack
.iter()
.rev()
.map(|index| self.parents[*index]),
) {
return;
}
@@ -2205,6 +2338,9 @@ impl<'a> Checker<'a> {
where
'b: 'a,
{
if self.settings.enabled.contains(&CheckCode::B021) {
flake8_bugbear::plugins::f_string_docstring(self, python_ast);
}
let docstring = docstrings::extraction::docstring_from(python_ast);
self.definitions.push((
Definition {
@@ -2222,8 +2358,8 @@ impl<'a> Checker<'a> {
fn check_deferred_annotations(&mut self) {
while let Some((expr, scopes, parents)) = self.deferred_annotations.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.parent_stack = parents;
self.visit_expr(expr);
}
}
@@ -2232,10 +2368,14 @@ impl<'a> Checker<'a> {
where
'b: 'a,
{
while let Some((range, expression)) = self.deferred_string_annotations.pop() {
let mut stacks = vec![];
while let Some((range, expression, scopes, parents)) =
self.deferred_string_annotations.pop()
{
if let Ok(mut expr) = parser::parse_expression(expression, "<filename>") {
relocate_expr(&mut expr, range);
allocator.push(expr);
stacks.push((scopes, parents));
} else {
if self.settings.enabled.contains(&CheckCode::F722) {
self.add_check(Check::new(
@@ -2245,7 +2385,9 @@ impl<'a> Checker<'a> {
}
}
}
for expr in allocator {
for (expr, (scopes, parents)) in allocator.iter().zip(stacks) {
self.scope_stack = scopes;
self.parent_stack = parents;
self.visit_expr(expr);
}
}
@@ -2371,7 +2513,7 @@ impl<'a> Checker<'a> {
let mut unused: BTreeMap<(ImportKind, usize, Option<usize>), Vec<&str>> =
BTreeMap::new();
for (name, binding) in scope.values.iter().rev() {
for (name, binding) in scope.values.iter() {
let used = binding.used.is_some()
|| all_names
.map(|names| names.contains(name))
@@ -2380,25 +2522,25 @@ impl<'a> Checker<'a> {
if !used {
match &binding.kind {
BindingKind::FromImportation(_, full_name, context) => {
let full_names = unused
unused
.entry((
ImportKind::ImportFrom,
context.defined_by,
context.defined_in,
))
.or_default();
full_names.push(full_name);
.or_default()
.push(full_name);
}
BindingKind::Importation(_, full_name, context)
| BindingKind::SubmoduleImportation(_, full_name, context) => {
let full_names = unused
unused
.entry((
ImportKind::Import,
context.defined_by,
context.defined_in,
))
.or_default();
full_names.push(full_name);
.or_default()
.push(full_name);
}
_ => {}
}
@@ -2439,7 +2581,7 @@ impl<'a> Checker<'a> {
if self.path.ends_with("__init__.py") {
checks.push(Check::new(
CheckKind::UnusedImport(
full_names.into_iter().map(String::from).collect(),
full_names.into_iter().sorted().map(String::from).collect(),
true,
),
Range::from_located(child),
@@ -2447,7 +2589,7 @@ impl<'a> Checker<'a> {
} else {
let mut check = Check::new(
CheckKind::UnusedImport(
full_names.into_iter().map(String::from).collect(),
full_names.into_iter().sorted().map(String::from).collect(),
false,
),
Range::from_located(child),
@@ -2640,8 +2782,5 @@ pub fn check_ast(
// Check docstrings.
checker.check_definitions();
// Check import blocks.
// checker.check_import_blocks();
checker.checks
}

View File

@@ -283,7 +283,7 @@ mod tests {
},
&fixer::Mode::Generate,
);
return checks;
checks
};
assert!(!check_with_max_line_length(6).is_empty());
assert!(check_with_max_line_length(7).is_empty());

View File

@@ -87,6 +87,7 @@ pub enum CheckCode {
B009,
B010,
B011,
B012,
B013,
B014,
B015,
@@ -94,8 +95,13 @@ pub enum CheckCode {
B017,
B018,
B019,
B020,
B021,
B022,
B024,
B025,
B026,
B027,
// flake8-comprehensions
C400,
C401,
@@ -220,6 +226,13 @@ pub enum CheckCode {
N818,
// isort
I001,
// flake8-bandit
S101,
S102,
S104,
S105,
S106,
S107,
// Ruff
RUF001,
RUF002,
@@ -236,6 +249,7 @@ pub enum CheckCategory {
Pydocstyle,
Pyupgrade,
PEP8Naming,
Flake8Bandit,
Flake8Comprehensions,
Flake8Bugbear,
Flake8Builtins,
@@ -253,6 +267,7 @@ impl CheckCategory {
CheckCategory::Pycodestyle => "pycodestyle",
CheckCategory::Pyflakes => "Pyflakes",
CheckCategory::Isort => "isort",
CheckCategory::Flake8Bandit => "flake8-bandit",
CheckCategory::Flake8Builtins => "flake8-builtins",
CheckCategory::Flake8Bugbear => "flake8-bugbear",
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
@@ -291,6 +306,7 @@ impl CheckCategory {
CheckCategory::Pyupgrade => Some("https://pypi.org/project/pyupgrade/3.2.0/"),
CheckCategory::Pydocstyle => Some("https://pypi.org/project/pydocstyle/6.1.1/"),
CheckCategory::PEP8Naming => Some("https://pypi.org/project/pep8-naming/0.13.2/"),
CheckCategory::Flake8Bandit => Some("https://pypi.org/project/flake8-bandit/4.1.1/"),
CheckCategory::Ruff => None,
CheckCategory::Meta => None,
}
@@ -372,10 +388,11 @@ pub enum CheckKind {
StripWithMultiCharacters,
MutableArgumentDefault,
UnusedLoopControlVariable(String),
FunctionCallArgumentDefault,
FunctionCallArgumentDefault(Option<String>),
GetAttrWithConstant,
SetAttrWithConstant,
DoNotAssertFalse,
JumpStatementInFinally(String),
RedundantTupleInExceptionHandler(String),
DuplicateHandlerException(Vec<String>),
UselessComparison,
@@ -383,8 +400,13 @@ pub enum CheckKind {
NoAssertRaisesException,
UselessExpression,
CachedInstanceMethod,
LoopVariableOverridesIterator(String),
FStringDocstring,
UselessContextlibSuppress,
AbstractBaseClassWithoutAbstractMethod(String),
DuplicateTryBlockException(String),
StarArgUnpackingAfterKeywordArg,
EmptyMethodWithoutAbstractDecorator(String),
// flake8-comprehensions
UnnecessaryGeneratorList,
UnnecessaryGeneratorSet,
@@ -509,6 +531,13 @@ pub enum CheckKind {
ErrorSuffixOnExceptionName(String),
// isort
UnsortedImports,
// flake8-bandit
AssertUsed,
ExecUsed,
HardcodedBindAllInterfaces,
HardcodedPasswordString(String),
HardcodedPasswordFuncArg(String),
HardcodedPasswordDefault(String),
// Ruff
AmbiguousUnicodeCharacterString(char, char),
AmbiguousUnicodeCharacterDocstring(char, char),
@@ -602,10 +631,13 @@ impl CheckCode {
CheckCode::B005 => CheckKind::StripWithMultiCharacters,
CheckCode::B006 => CheckKind::MutableArgumentDefault,
CheckCode::B007 => CheckKind::UnusedLoopControlVariable("i".to_string()),
CheckCode::B008 => CheckKind::FunctionCallArgumentDefault,
CheckCode::B008 => CheckKind::FunctionCallArgumentDefault(None),
CheckCode::B009 => CheckKind::GetAttrWithConstant,
CheckCode::B010 => CheckKind::SetAttrWithConstant,
CheckCode::B011 => CheckKind::DoNotAssertFalse,
CheckCode::B012 => {
CheckKind::JumpStatementInFinally("return/continue/break".to_string())
}
CheckCode::B013 => {
CheckKind::RedundantTupleInExceptionHandler("ValueError".to_string())
}
@@ -615,8 +647,13 @@ impl CheckCode {
CheckCode::B017 => CheckKind::NoAssertRaisesException,
CheckCode::B018 => CheckKind::UselessExpression,
CheckCode::B019 => CheckKind::CachedInstanceMethod,
CheckCode::B020 => CheckKind::LoopVariableOverridesIterator("...".to_string()),
CheckCode::B021 => CheckKind::FStringDocstring,
CheckCode::B022 => CheckKind::UselessContextlibSuppress,
CheckCode::B024 => CheckKind::AbstractBaseClassWithoutAbstractMethod("...".to_string()),
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg,
CheckCode::B027 => CheckKind::EmptyMethodWithoutAbstractDecorator("...".to_string()),
// flake8-comprehensions
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
CheckCode::C401 => CheckKind::UnnecessaryGeneratorSet,
@@ -773,6 +810,13 @@ impl CheckCode {
CheckCode::N818 => CheckKind::ErrorSuffixOnExceptionName("...".to_string()),
// isort
CheckCode::I001 => CheckKind::UnsortedImports,
// flake8-bandit
CheckCode::S101 => CheckKind::AssertUsed,
CheckCode::S102 => CheckKind::ExecUsed,
CheckCode::S104 => CheckKind::HardcodedBindAllInterfaces,
CheckCode::S105 => CheckKind::HardcodedPasswordString("...".to_string()),
CheckCode::S106 => CheckKind::HardcodedPasswordFuncArg("...".to_string()),
CheckCode::S107 => CheckKind::HardcodedPasswordDefault("...".to_string()),
// Ruff
CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'),
CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'),
@@ -841,6 +885,7 @@ impl CheckCode {
CheckCode::B009 => CheckCategory::Flake8Bugbear,
CheckCode::B010 => CheckCategory::Flake8Bugbear,
CheckCode::B011 => CheckCategory::Flake8Bugbear,
CheckCode::B012 => CheckCategory::Flake8Bugbear,
CheckCode::B013 => CheckCategory::Flake8Bugbear,
CheckCode::B014 => CheckCategory::Flake8Bugbear,
CheckCode::B015 => CheckCategory::Flake8Bugbear,
@@ -848,8 +893,13 @@ impl CheckCode {
CheckCode::B017 => CheckCategory::Flake8Bugbear,
CheckCode::B018 => CheckCategory::Flake8Bugbear,
CheckCode::B019 => CheckCategory::Flake8Bugbear,
CheckCode::B020 => CheckCategory::Flake8Bugbear,
CheckCode::B021 => CheckCategory::Flake8Bugbear,
CheckCode::B022 => CheckCategory::Flake8Bugbear,
CheckCode::B024 => CheckCategory::Flake8Bugbear,
CheckCode::B025 => CheckCategory::Flake8Bugbear,
CheckCode::B026 => CheckCategory::Flake8Bugbear,
CheckCode::B027 => CheckCategory::Flake8Bugbear,
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
CheckCode::C402 => CheckCategory::Flake8Comprehensions,
@@ -965,6 +1015,12 @@ impl CheckCode {
CheckCode::N817 => CheckCategory::PEP8Naming,
CheckCode::N818 => CheckCategory::PEP8Naming,
CheckCode::I001 => CheckCategory::Isort,
CheckCode::S101 => CheckCategory::Flake8Bandit,
CheckCode::S102 => CheckCategory::Flake8Bandit,
CheckCode::S104 => CheckCategory::Flake8Bandit,
CheckCode::S105 => CheckCategory::Flake8Bandit,
CheckCode::S106 => CheckCategory::Flake8Bandit,
CheckCode::S107 => CheckCategory::Flake8Bandit,
CheckCode::RUF001 => CheckCategory::Ruff,
CheckCode::RUF002 => CheckCategory::Ruff,
CheckCode::RUF003 => CheckCategory::Ruff,
@@ -1034,10 +1090,11 @@ impl CheckKind {
CheckKind::StripWithMultiCharacters => &CheckCode::B005,
CheckKind::MutableArgumentDefault => &CheckCode::B006,
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
CheckKind::FunctionCallArgumentDefault => &CheckCode::B008,
CheckKind::FunctionCallArgumentDefault(_) => &CheckCode::B008,
CheckKind::GetAttrWithConstant => &CheckCode::B009,
CheckKind::SetAttrWithConstant => &CheckCode::B010,
CheckKind::DoNotAssertFalse => &CheckCode::B011,
CheckKind::JumpStatementInFinally(_) => &CheckCode::B012,
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
CheckKind::UselessComparison => &CheckCode::B015,
@@ -1045,8 +1102,13 @@ impl CheckKind {
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::UselessExpression => &CheckCode::B018,
CheckKind::CachedInstanceMethod => &CheckCode::B019,
CheckKind::LoopVariableOverridesIterator(_) => &CheckCode::B020,
CheckKind::FStringDocstring => &CheckCode::B021,
CheckKind::UselessContextlibSuppress => &CheckCode::B022,
CheckKind::AbstractBaseClassWithoutAbstractMethod(_) => &CheckCode::B024,
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
CheckKind::EmptyMethodWithoutAbstractDecorator(_) => &CheckCode::B027,
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
CheckKind::UnnecessaryGeneratorSet => &CheckCode::C401,
@@ -1171,6 +1233,13 @@ impl CheckKind {
CheckKind::ErrorSuffixOnExceptionName(..) => &CheckCode::N818,
// isort
CheckKind::UnsortedImports => &CheckCode::I001,
// flake8-bandit
CheckKind::AssertUsed => &CheckCode::S101,
CheckKind::ExecUsed => &CheckCode::S102,
CheckKind::HardcodedBindAllInterfaces => &CheckCode::S104,
CheckKind::HardcodedPasswordString(..) => &CheckCode::S105,
CheckKind::HardcodedPasswordFuncArg(..) => &CheckCode::S106,
CheckKind::HardcodedPasswordDefault(..) => &CheckCode::S107,
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001,
CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002,
@@ -1199,7 +1268,7 @@ impl CheckKind {
CheckKind::BreakOutsideLoop => "`break` outside loop".to_string(),
CheckKind::ContinueOutsideLoop => "`continue` not properly in loop".to_string(),
CheckKind::DefaultExceptNotLast => {
"An `except:` block as not the last exception handler".to_string()
"An `except` block as not the last exception handler".to_string()
}
CheckKind::DoNotAssignLambda => {
"Do not assign a lambda expression, use a def".to_string()
@@ -1336,40 +1405,47 @@ impl CheckKind {
`+(+(n))`, which equals `n`. You meant `n += 1`."
.to_string(),
CheckKind::AssignmentToOsEnviron => {
"Assigning to `os.environ` doesn't clear the environment.".to_string()
"Assigning to `os.environ` doesn't clear the environment".to_string()
}
CheckKind::UnreliableCallableCheck => " Using `hasattr(x, '__call__')` to test if x \
is callable is unreliable. Use `callable(x)` \
for consistent results."
.to_string(),
CheckKind::StripWithMultiCharacters => "Using `.strip()` with multi-character strings \
is misleading the reader."
.to_string(),
CheckKind::StripWithMultiCharacters => {
"Using `.strip()` with multi-character strings is misleading the reader".to_string()
}
CheckKind::MutableArgumentDefault => {
"Do not use mutable data structures for argument defaults.".to_string()
"Do not use mutable data structures for argument defaults".to_string()
}
CheckKind::UnusedLoopControlVariable(name) => format!(
"Loop control variable `{name}` not used within the loop body. If this is \
intended, start the name with an underscore."
),
CheckKind::FunctionCallArgumentDefault => {
"Do not perform function calls in argument defaults.".to_string()
CheckKind::FunctionCallArgumentDefault(name) => {
if let Some(name) = name {
format!("Do not perform function call `{name}` in argument defaults")
} else {
"Do not perform function call in argument defaults".to_string()
}
}
CheckKind::GetAttrWithConstant => "Do not call `getattr` with a constant attribute \
value, it is not any safer than normal property \
value. It is not any safer than normal property \
access."
.to_string(),
CheckKind::SetAttrWithConstant => "Do not call `setattr` with a constant attribute \
value, it is not any safer than normal property \
value. It is not any safer than normal property \
access."
.to_string(),
CheckKind::DoNotAssertFalse => "Do not `assert False` (`python -O` removes these \
calls), raise `AssertionError()`"
.to_string(),
CheckKind::JumpStatementInFinally(name) => {
format!("`{name}` inside finally blocks cause exceptions to be silenced")
}
CheckKind::RedundantTupleInExceptionHandler(name) => {
format!(
"A length-one tuple literal is redundant. Write `except {name}:` instead of \
`except ({name},):`."
"A length-one tuple literal is redundant. Write `except {name}` instead of \
`except ({name},)`."
)
}
CheckKind::UselessComparison => "Pointless comparison. This comparison does nothing \
@@ -1389,7 +1465,7 @@ impl CheckKind {
}
}
CheckKind::NoAssertRaisesException => {
"`assertRaises(Exception):` should be considered evil. It can lead to your test \
"`assertRaises(Exception)` should be considered evil. It can lead to your test \
passing even if the code being tested is never executed due to a typo. Either \
assert for a more specific exception (builtin or custom), use \
`assertRaisesRegex`, or use the context manager form of `assertRaises`."
@@ -1399,18 +1475,37 @@ impl CheckKind {
"Found useless expression. Either assign it to a variable or remove it.".to_string()
}
CheckKind::CachedInstanceMethod => "Use of `functools.lru_cache` or `functools.cache` \
on methods can lead to memory leaks."
on methods can lead to memory leaks"
.to_string(),
CheckKind::LoopVariableOverridesIterator(name) => {
format!("Loop control variable `{name}` overrides iterable it iterates")
}
CheckKind::FStringDocstring => "f-string used as docstring. This will be interpreted \
by python as a joined string rather than a docstring."
.to_string(),
CheckKind::UselessContextlibSuppress => {
"No arguments passed to `contextlib.suppress`. No exceptions will be suppressed \
and therefore this context manager is redundant"
.to_string()
}
CheckKind::AbstractBaseClassWithoutAbstractMethod(name) => {
format!("`{name}` is an abstract base class, but it has no abstract methods")
}
CheckKind::DuplicateTryBlockException(name) => {
format!("try-except block with duplicate exception `{name}`")
}
CheckKind::StarArgUnpackingAfterKeywordArg => {
"Star-arg unpacking after a keyword argument is strongly discouraged, because it \
only works when the keyword parameter is declared after all parameters supplied \
by the unpacked sequence, and this change of ordering can surprise and mislead \
readers."
"Star-arg unpacking after a keyword argument is strongly discouraged. It only \
works when the keyword parameter is declared after all parameters supplied by the \
unpacked sequence, and this change of ordering can surprise and mislead readers."
.to_string()
}
CheckKind::EmptyMethodWithoutAbstractDecorator(name) => {
format!(
"`{name}` is an empty method in an abstract base class, but has no abstract \
decorator"
)
}
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => {
"Unnecessary generator (rewrite as a `list` comprehension)".to_string()
@@ -1610,7 +1705,7 @@ impl CheckKind {
}
}
CheckKind::UnnecessaryLRUCacheParams => {
"Unnecessary parameters to functools.lru_cache".to_string()
"Unnecessary parameters to `functools.lru_cache`".to_string()
}
CheckKind::UnnecessaryEncodeUTF8 => "Unnecessary call to `encode` as UTF-8".to_string(),
// pydocstyle
@@ -1666,7 +1761,7 @@ impl CheckKind {
CheckKind::PublicNestedClass => "Missing docstring in public nested class".to_string(),
CheckKind::PublicInit => "Missing docstring in `__init__`".to_string(),
CheckKind::NoThisPrefix => {
"First word of the docstring should not be 'This'".to_string()
"First word of the docstring should not be \"This\"".to_string()
}
CheckKind::SkipDocstring => {
"Function decorated with `@overload` shouldn't contain a docstring".to_string()
@@ -1774,10 +1869,25 @@ impl CheckKind {
format!("Exception name `{name}` should be named with an Error suffix")
}
CheckKind::PEP3120UnnecessaryCodingComment => {
"utf-8 encoding declaration is unnecessary".to_string()
"UTF-8 encoding declaration is unnecessary".to_string()
}
// isort
CheckKind::UnsortedImports => "Import block is un-sorted or un-formatted".to_string(),
// flake8-bandit
CheckKind::AssertUsed => "Use of `assert` detected".to_string(),
CheckKind::ExecUsed => "Use of `exec` detected".to_string(),
CheckKind::HardcodedBindAllInterfaces => {
"Possible binding to all interfaces".to_string()
}
CheckKind::HardcodedPasswordString(string) => {
format!("Possible hardcoded password: `\"{string}\"`")
}
CheckKind::HardcodedPasswordFuncArg(string) => {
format!("Possible hardcoded password: `\"{string}\"`")
}
CheckKind::HardcodedPasswordDefault(string) => {
format!("Possible hardcoded password: `\"{string}\"`")
}
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => {
format!(
@@ -1822,16 +1932,16 @@ impl CheckKind {
pub fn summary(&self) -> String {
match self {
CheckKind::UnaryPrefixIncrement => {
"Python does not support the unary prefix increment.".to_string()
"Python does not support the unary prefix increment".to_string()
}
CheckKind::UnusedLoopControlVariable(name) => {
format!("Loop control variable `{name}` not used within the loop body.")
format!("Loop control variable `{name}` not used within the loop body")
}
CheckKind::NoAssertRaisesException => {
"`assertRaises(Exception):` should be considered evil.".to_string()
"`assertRaises(Exception)` should be considered evil".to_string()
}
CheckKind::StarArgUnpackingAfterKeywordArg => {
"Star-arg unpacking after a keyword argument is strongly discouraged.".to_string()
"Star-arg unpacking after a keyword argument is strongly discouraged".to_string()
}
_ => self.body(),
}

View File

@@ -47,6 +47,7 @@ pub enum CheckCodePrefix {
B01,
B010,
B011,
B012,
B013,
B014,
B015,
@@ -55,8 +56,13 @@ pub enum CheckCodePrefix {
B018,
B019,
B02,
B020,
B021,
B022,
B024,
B025,
B026,
B027,
C,
C4,
C40,
@@ -245,6 +251,15 @@ pub enum CheckCodePrefix {
RUF001,
RUF002,
RUF003,
S,
S1,
S10,
S101,
S102,
S104,
S105,
S106,
S107,
T,
T2,
T20,
@@ -364,6 +379,7 @@ impl CheckCodePrefix {
CheckCode::B009,
CheckCode::B010,
CheckCode::B011,
CheckCode::B012,
CheckCode::B013,
CheckCode::B014,
CheckCode::B015,
@@ -371,8 +387,13 @@ impl CheckCodePrefix {
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
],
CheckCodePrefix::B0 => vec![
CheckCode::B002,
@@ -385,6 +406,7 @@ impl CheckCodePrefix {
CheckCode::B009,
CheckCode::B010,
CheckCode::B011,
CheckCode::B012,
CheckCode::B013,
CheckCode::B014,
CheckCode::B015,
@@ -392,8 +414,13 @@ impl CheckCodePrefix {
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
],
CheckCodePrefix::B00 => vec![
CheckCode::B002,
@@ -416,6 +443,7 @@ impl CheckCodePrefix {
CheckCodePrefix::B01 => vec![
CheckCode::B010,
CheckCode::B011,
CheckCode::B012,
CheckCode::B013,
CheckCode::B014,
CheckCode::B015,
@@ -426,6 +454,7 @@ impl CheckCodePrefix {
],
CheckCodePrefix::B010 => vec![CheckCode::B010],
CheckCodePrefix::B011 => vec![CheckCode::B011],
CheckCodePrefix::B012 => vec![CheckCode::B012],
CheckCodePrefix::B013 => vec![CheckCode::B013],
CheckCodePrefix::B014 => vec![CheckCode::B014],
CheckCodePrefix::B015 => vec![CheckCode::B015],
@@ -433,9 +462,22 @@ impl CheckCodePrefix {
CheckCodePrefix::B017 => vec![CheckCode::B017],
CheckCodePrefix::B018 => vec![CheckCode::B018],
CheckCodePrefix::B019 => vec![CheckCode::B019],
CheckCodePrefix::B02 => vec![CheckCode::B025, CheckCode::B026],
CheckCodePrefix::B02 => vec![
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
],
CheckCodePrefix::B020 => vec![CheckCode::B020],
CheckCodePrefix::B021 => vec![CheckCode::B021],
CheckCodePrefix::B022 => vec![CheckCode::B022],
CheckCodePrefix::B024 => vec![CheckCode::B024],
CheckCodePrefix::B025 => vec![CheckCode::B025],
CheckCodePrefix::B026 => vec![CheckCode::B026],
CheckCodePrefix::B027 => vec![CheckCode::B027],
CheckCodePrefix::C => vec![
CheckCode::C400,
CheckCode::C401,
@@ -988,6 +1030,36 @@ impl CheckCodePrefix {
CheckCodePrefix::RUF001 => vec![CheckCode::RUF001],
CheckCodePrefix::RUF002 => vec![CheckCode::RUF002],
CheckCodePrefix::RUF003 => vec![CheckCode::RUF003],
CheckCodePrefix::S => vec![
CheckCode::S101,
CheckCode::S102,
CheckCode::S104,
CheckCode::S105,
CheckCode::S106,
CheckCode::S107,
],
CheckCodePrefix::S1 => vec![
CheckCode::S101,
CheckCode::S102,
CheckCode::S104,
CheckCode::S105,
CheckCode::S106,
CheckCode::S107,
],
CheckCodePrefix::S10 => vec![
CheckCode::S101,
CheckCode::S102,
CheckCode::S104,
CheckCode::S105,
CheckCode::S106,
CheckCode::S107,
],
CheckCodePrefix::S101 => vec![CheckCode::S101],
CheckCodePrefix::S102 => vec![CheckCode::S102],
CheckCodePrefix::S104 => vec![CheckCode::S104],
CheckCodePrefix::S105 => vec![CheckCode::S105],
CheckCodePrefix::S106 => vec![CheckCode::S106],
CheckCodePrefix::S107 => vec![CheckCode::S107],
CheckCodePrefix::T => vec![CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T2 => vec![CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T20 => vec![CheckCode::T201, CheckCode::T203],
@@ -1137,6 +1209,7 @@ impl CheckCodePrefix {
CheckCodePrefix::B01 => PrefixSpecificity::Tens,
CheckCodePrefix::B010 => PrefixSpecificity::Explicit,
CheckCodePrefix::B011 => PrefixSpecificity::Explicit,
CheckCodePrefix::B012 => PrefixSpecificity::Explicit,
CheckCodePrefix::B013 => PrefixSpecificity::Explicit,
CheckCodePrefix::B014 => PrefixSpecificity::Explicit,
CheckCodePrefix::B015 => PrefixSpecificity::Explicit,
@@ -1145,8 +1218,13 @@ impl CheckCodePrefix {
CheckCodePrefix::B018 => PrefixSpecificity::Explicit,
CheckCodePrefix::B019 => PrefixSpecificity::Explicit,
CheckCodePrefix::B02 => PrefixSpecificity::Tens,
CheckCodePrefix::B020 => PrefixSpecificity::Explicit,
CheckCodePrefix::B021 => PrefixSpecificity::Explicit,
CheckCodePrefix::B022 => PrefixSpecificity::Explicit,
CheckCodePrefix::B024 => PrefixSpecificity::Explicit,
CheckCodePrefix::B025 => PrefixSpecificity::Explicit,
CheckCodePrefix::B026 => PrefixSpecificity::Explicit,
CheckCodePrefix::B027 => PrefixSpecificity::Explicit,
CheckCodePrefix::C => PrefixSpecificity::Category,
CheckCodePrefix::C4 => PrefixSpecificity::Hundreds,
CheckCodePrefix::C40 => PrefixSpecificity::Tens,
@@ -1335,6 +1413,15 @@ impl CheckCodePrefix {
CheckCodePrefix::RUF001 => PrefixSpecificity::Explicit,
CheckCodePrefix::RUF002 => PrefixSpecificity::Explicit,
CheckCodePrefix::RUF003 => PrefixSpecificity::Explicit,
CheckCodePrefix::S => PrefixSpecificity::Category,
CheckCodePrefix::S1 => PrefixSpecificity::Hundreds,
CheckCodePrefix::S10 => PrefixSpecificity::Tens,
CheckCodePrefix::S101 => PrefixSpecificity::Explicit,
CheckCodePrefix::S102 => PrefixSpecificity::Explicit,
CheckCodePrefix::S104 => PrefixSpecificity::Explicit,
CheckCodePrefix::S105 => PrefixSpecificity::Explicit,
CheckCodePrefix::S106 => PrefixSpecificity::Explicit,
CheckCodePrefix::S107 => PrefixSpecificity::Explicit,
CheckCodePrefix::T => PrefixSpecificity::Category,
CheckCodePrefix::T2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::T20 => PrefixSpecificity::Tens,

View File

@@ -1,8 +1,8 @@
use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
use clap::{command, Parser};
use fnv::FnvHashMap;
use log::warn;
use regex::Regex;
@@ -87,6 +87,10 @@ pub struct Cli {
/// The minimum Python version that should be supported.
#[arg(long)]
pub target_version: Option<PythonVersion>,
/// Set the line-length for length-associated checks and automatic
/// formatting.
#[arg(long)]
pub line_length: Option<usize>,
/// Round-trip auto-formatting.
// TODO(charlie): This should be a sub-command.
#[arg(long, hide = true)]
@@ -120,6 +124,8 @@ pub fn extract_log_level(cli: &Cli) -> LogLevel {
LogLevel::Quiet
} else if cli.verbose {
LogLevel::Verbose
} else if matches!(cli.format, SerializationFormat::Json) {
LogLevel::Quiet
} else {
LogLevel::Default
}
@@ -188,7 +194,7 @@ pub fn collect_per_file_ignores(
pairs: Vec<PatternPrefixPair>,
project_root: &Option<PathBuf>,
) -> Vec<PerFileIgnore> {
let mut per_file_ignores: BTreeMap<String, Vec<CheckCodePrefix>> = BTreeMap::new();
let mut per_file_ignores: FnvHashMap<String, Vec<CheckCodePrefix>> = FnvHashMap::default();
for pair in pairs {
per_file_ignores
.entry(pair.pattern)

View File

@@ -1,11 +1,10 @@
//! Abstractions for Google-style docstrings.
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use once_cell::sync::Lazy;
pub(crate) static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
pub(crate) static GOOGLE_SECTION_NAMES: Lazy<FnvHashSet<&'static str>> = Lazy::new(|| {
FnvHashSet::from_iter([
"Args",
"Arguments",
"Attention",
@@ -37,35 +36,36 @@ pub(crate) static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new
])
});
pub(crate) static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"args",
"arguments",
"attention",
"attributes",
"caution",
"danger",
"error",
"example",
"examples",
"hint",
"important",
"keyword args",
"keyword arguments",
"methods",
"note",
"notes",
"return",
"returns",
"raises",
"references",
"see also",
"tip",
"todo",
"warning",
"warnings",
"warns",
"yield",
"yields",
])
});
pub(crate) static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy<FnvHashSet<&'static str>> =
Lazy::new(|| {
FnvHashSet::from_iter([
"args",
"arguments",
"attention",
"attributes",
"caution",
"danger",
"error",
"example",
"examples",
"hint",
"important",
"keyword args",
"keyword arguments",
"methods",
"note",
"notes",
"return",
"returns",
"raises",
"references",
"see also",
"tip",
"todo",
"warning",
"warnings",
"warns",
"yield",
"yields",
])
});

View File

@@ -1,11 +1,10 @@
//! Abstractions for NumPy-style docstrings.
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use once_cell::sync::Lazy;
pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<FnvHashSet<&'static str>> = Lazy::new(|| {
FnvHashSet::from_iter([
"short summary",
"extended summary",
"parameters",
@@ -22,8 +21,8 @@ pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> =
])
});
pub(crate) static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
pub(crate) static NUMPY_SECTION_NAMES: Lazy<FnvHashSet<&'static str>> = Lazy::new(|| {
FnvHashSet::from_iter([
"Short Summary",
"Extended Summary",
"Parameters",

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use once_cell::sync::Lazy;
use crate::docstrings::google::{GOOGLE_SECTION_NAMES, LOWERCASE_GOOGLE_SECTION_NAMES};
@@ -11,14 +10,14 @@ pub(crate) enum SectionStyle {
}
impl SectionStyle {
pub(crate) fn section_names(&self) -> &Lazy<BTreeSet<&'static str>> {
pub(crate) fn section_names(&self) -> &Lazy<FnvHashSet<&'static str>> {
match self {
SectionStyle::NumPy => &NUMPY_SECTION_NAMES,
SectionStyle::Google => &GOOGLE_SECTION_NAMES,
}
}
pub(crate) fn lowercase_section_names(&self) -> &Lazy<BTreeSet<&'static str>> {
pub(crate) fn lowercase_section_names(&self) -> &Lazy<FnvHashSet<&'static str>> {
match self {
SectionStyle::NumPy => &LOWERCASE_NUMPY_SECTION_NAMES,
SectionStyle::Google => &LOWERCASE_GOOGLE_SECTION_NAMES,

View File

@@ -1,13 +1,19 @@
use num_bigint::BigInt;
use rustpython_ast::{Cmpop, Constant, Expr, ExprKind, Located};
use crate::ast::helpers::match_name_or_attr_from_module;
use crate::ast::helpers::match_module_member;
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
fn is_sys(checker: &Checker, expr: &Expr, target: &str) -> bool {
match_name_or_attr_from_module(expr, target, "sys", checker.from_imports.get("sys"))
match_module_member(
expr,
"sys",
target,
&checker.from_imports,
&checker.import_aliases,
)
}
/// YTT101, YTT102, YTT301, YTT303
@@ -181,9 +187,13 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
/// YTT202
pub fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
if match_name_or_attr_from_module(expr, "PY3", "six", checker.from_imports.get("six"))
&& checker.settings.enabled.contains(&CheckCode::YTT202)
{
if match_module_member(
expr,
"six",
"PY3",
&checker.from_imports,
&checker.import_aliases,
) {
checker.add_check(Check::new(
CheckKind::SixPY3Referenced,
Range::from_located(expr),

View File

@@ -49,10 +49,13 @@ fn is_none_returning(body: &[Stmt]) -> bool {
}
/// ANN401
fn check_dynamically_typed(checker: &mut Checker, annotation: &Expr, name: &str) {
if checker.match_typing_module(annotation, "Any") {
fn check_dynamically_typed<F>(checker: &mut Checker, annotation: &Expr, func: F)
where
F: FnOnce() -> String,
{
if checker.match_typing_expr(annotation, "Any") {
checker.add_check(Check::new(
CheckKind::DynamicallyTypedExpression(name.to_string()),
CheckKind::DynamicallyTypedExpression(func()),
Range::from_located(annotation),
));
};
@@ -100,7 +103,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
{
if let Some(expr) = &arg.node.annotation {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, expr, &arg.node.arg);
check_dynamically_typed(checker, expr, || arg.node.arg.to_string());
};
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
@@ -122,7 +125,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("*{name}"));
check_dynamically_typed(checker, expr, || format!("*{name}"));
}
}
} else {
@@ -145,7 +148,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("**{name}"));
check_dynamically_typed(checker, expr, || format!("**{name}"));
}
}
} else {
@@ -165,7 +168,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, expr, name);
check_dynamically_typed(checker, expr, || name.to_string());
};
} else {
// Allow omission of return annotation in `__init__` functions, if the function
@@ -215,7 +218,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if let Some(annotation) = &arg.node.annotation {
has_any_typed_arg = true;
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, annotation, &arg.node.arg);
check_dynamically_typed(checker, annotation, || arg.node.arg.to_string());
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
@@ -238,7 +241,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("*{name}"));
check_dynamically_typed(checker, expr, || format!("*{name}"));
}
}
} else {
@@ -262,7 +265,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
let name = arg.node.arg.to_string();
check_dynamically_typed(checker, expr, &format!("**{name}"));
check_dynamically_typed(checker, expr, || format!("**{name}"));
}
}
} else {
@@ -305,7 +308,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
// ANN201, ANN202
if let Some(expr) = &returns {
if checker.settings.enabled.contains(&CheckCode::ANN401) {
check_dynamically_typed(checker, expr, name);
check_dynamically_typed(checker, expr, || name.to_string());
}
} else {
// Allow omission of return annotation in `__init__` functions, if the function

View File

@@ -0,0 +1,22 @@
use rustpython_ast::{Constant, Expr, ExprKind};
const PASSWORD_NAMES: [&str; 7] = [
"password", "pass", "passwd", "pwd", "secret", "token", "secrete",
];
pub fn string_literal(expr: &Expr) -> Option<&str> {
match &expr.node {
ExprKind::Constant {
value: Constant::Str(string),
..
} => Some(string),
_ => None,
}
}
// Maybe use regex for this?
pub fn matches_password_name(string: &str) -> bool {
PASSWORD_NAMES
.iter()
.any(|name| string.to_lowercase().contains(name))
}

2
src/flake8_bandit/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
mod helpers;
pub mod plugins;

View File

@@ -0,0 +1,9 @@
use rustpython_ast::{Located, StmtKind};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
/// S101
pub fn assert_used(stmt: &Located<StmtKind>) -> Check {
Check::new(CheckKind::AssertUsed, Range::from_located(stmt))
}

View File

@@ -0,0 +1,14 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
/// S102
pub fn exec_used(expr: &Expr, func: &Expr) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "exec" {
return Some(Check::new(CheckKind::ExecUsed, Range::from_located(expr)));
}
}
None
}

View File

@@ -0,0 +1,11 @@
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
/// S104
pub fn hardcoded_bind_all_interfaces(value: &str, range: &Range) -> Option<Check> {
if value == "0.0.0.0" {
Some(Check::new(CheckKind::HardcodedBindAllInterfaces, *range))
} else {
None
}
}

View File

@@ -0,0 +1,51 @@
use rustpython_ast::{ArgData, Arguments, Expr, Located};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::flake8_bandit::helpers::{matches_password_name, string_literal};
fn check_password_kwarg(arg: &Located<ArgData>, default: &Expr) -> Option<Check> {
if let Some(string) = string_literal(default) {
let kwarg_name = &arg.node.arg;
if matches_password_name(kwarg_name) {
return Some(Check::new(
CheckKind::HardcodedPasswordDefault(string.to_string()),
Range::from_located(default),
));
}
}
None
}
/// S107
pub fn hardcoded_password_default(arguments: &Arguments) -> Vec<Check> {
let mut checks: Vec<Check> = Vec::new();
let defaults_start =
arguments.posonlyargs.len() + arguments.args.len() - arguments.defaults.len();
for (i, arg) in arguments
.posonlyargs
.iter()
.chain(&arguments.args)
.enumerate()
{
if let Some(i) = i.checked_sub(defaults_start) {
let default = &arguments.defaults[i];
if let Some(check) = check_password_kwarg(arg, default) {
checks.push(check);
}
}
}
let defaults_start = arguments.kwonlyargs.len() - arguments.kw_defaults.len();
for (i, kwarg) in arguments.kwonlyargs.iter().enumerate() {
if let Some(i) = i.checked_sub(defaults_start) {
let default = &arguments.kw_defaults[i];
if let Some(check) = check_password_kwarg(kwarg, default) {
checks.push(check);
}
}
}
checks
}

View File

@@ -0,0 +1,25 @@
use rustpython_ast::Keyword;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::flake8_bandit::helpers::{matches_password_name, string_literal};
/// S106
pub fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec<Check> {
keywords
.iter()
.filter_map(|keyword| {
if let Some(string) = string_literal(&keyword.node.value) {
if let Some(arg) = &keyword.node.arg {
if matches_password_name(arg) {
return Some(Check::new(
CheckKind::HardcodedPasswordFuncArg(string.to_string()),
Range::from_located(keyword),
));
}
}
}
None
})
.collect()
}

View File

@@ -0,0 +1,58 @@
use rustpython_ast::{Constant, Expr, ExprKind};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::flake8_bandit::helpers::{matches_password_name, string_literal};
fn is_password_target(target: &Expr) -> bool {
let target_name = match &target.node {
// variable = "s3cr3t"
ExprKind::Name { id, .. } => id,
// d["password"] = "s3cr3t"
ExprKind::Subscript { slice, .. } => match &slice.node {
ExprKind::Constant {
value: Constant::Str(string),
..
} => string,
_ => return false,
},
// obj.password = "s3cr3t"
ExprKind::Attribute { attr, .. } => attr,
_ => return false,
};
matches_password_name(target_name)
}
/// S105
pub fn compare_to_hardcoded_password_string(left: &Expr, comparators: &[Expr]) -> Vec<Check> {
comparators
.iter()
.filter_map(|comp| {
if let Some(string) = string_literal(comp) {
if is_password_target(left) {
return Some(Check::new(
CheckKind::HardcodedPasswordString(string.to_string()),
Range::from_located(comp),
));
}
}
None
})
.collect()
}
/// S105
pub fn assign_hardcoded_password_string(value: &Expr, targets: &Vec<Expr>) -> Option<Check> {
if let Some(string) = string_literal(value) {
for target in targets {
if is_password_target(target) {
return Some(Check::new(
CheckKind::HardcodedPasswordString(string.to_string()),
Range::from_located(value),
));
}
}
}
None
}

View File

@@ -0,0 +1,15 @@
pub use assert_used::assert_used;
pub use exec_used::exec_used;
pub use hardcoded_bind_all_interfaces::hardcoded_bind_all_interfaces;
pub use hardcoded_password_default::hardcoded_password_default;
pub use hardcoded_password_func_arg::hardcoded_password_func_arg;
pub use hardcoded_password_string::{
assign_hardcoded_password_string, compare_to_hardcoded_password_string,
};
mod assert_used;
mod exec_used;
mod hardcoded_bind_all_interfaces;
mod hardcoded_password_default;
mod hardcoded_password_func_arg;
mod hardcoded_password_string;

View File

@@ -1,2 +1,36 @@
mod constants;
pub mod plugins;
pub mod settings;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use crate::autofix::fixer;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::{flake8_bugbear, Settings};
#[test]
fn extend_immutable_calls() -> Result<()> {
let snapshot = "extend_immutable_calls".to_string();
let mut checks = test_path(
Path::new("./resources/test/fixtures/B008_extended.py"),
&Settings {
flake8_bugbear: flake8_bugbear::settings::Settings {
extend_immutable_calls: vec![
"fastapi.Depends".to_string(),
"fastapi.Query".to_string(),
],
},
..Settings::for_rules(vec![CheckCode::B008])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
}

View File

@@ -0,0 +1,129 @@
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::helpers::match_module_member;
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
fn is_abc_class(
bases: &[Expr],
keywords: &[Keyword],
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
keywords.iter().any(|keyword| {
keyword
.node
.arg
.as_ref()
.map(|a| a == "metaclass")
.unwrap_or(false)
&& match_module_member(
&keyword.node.value,
"abc",
"ABCMeta",
from_imports,
import_aliases,
)
}) || bases
.iter()
.any(|base| match_module_member(base, "abc", "ABC", from_imports, import_aliases))
}
fn is_empty_body(body: &[Stmt]) -> bool {
body.iter().all(|stmt| match &stmt.node {
StmtKind::Pass => true,
StmtKind::Expr { value } => match &value.node {
ExprKind::Constant { value, .. } => {
matches!(value, Constant::Str(..) | Constant::Ellipsis)
}
_ => false,
},
_ => false,
})
}
fn is_abstractmethod(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
match_module_member(expr, "abc", "abstractmethod", from_imports, import_aliases)
}
fn is_overload(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
match_module_member(expr, "typing", "overload", from_imports, import_aliases)
}
pub fn abstract_base_class(
checker: &mut Checker,
stmt: &Stmt,
name: &str,
bases: &[Expr],
keywords: &[Keyword],
body: &[Stmt],
) {
if bases.len() + keywords.len() == 1
&& is_abc_class(
bases,
keywords,
&checker.from_imports,
&checker.import_aliases,
)
{
let mut has_abstract_method = false;
for stmt in body {
// https://github.com/PyCQA/flake8-bugbear/issues/293
// Ignore abc's that declares a class attribute that must be set
if let StmtKind::AnnAssign { .. } | StmtKind::Assign { .. } = &stmt.node {
has_abstract_method = true;
continue;
}
if let StmtKind::FunctionDef {
decorator_list,
body,
..
}
| StmtKind::AsyncFunctionDef {
decorator_list,
body,
..
} = &stmt.node
{
let has_abstract_decorator = decorator_list
.iter()
.any(|d| is_abstractmethod(d, &checker.from_imports, &checker.import_aliases));
has_abstract_method |= has_abstract_decorator;
if checker.settings.enabled.contains(&CheckCode::B027) {
if !has_abstract_decorator
&& is_empty_body(body)
&& !decorator_list
.iter()
.any(|d| is_overload(d, &checker.from_imports, &checker.import_aliases))
{
checker.add_check(Check::new(
CheckKind::EmptyMethodWithoutAbstractDecorator(name.to_string()),
Range::from_located(stmt),
));
}
}
}
}
if checker.settings.enabled.contains(&CheckCode::B024) {
if !has_abstract_method {
checker.add_check(Check::new(
CheckKind::AbstractBaseClassWithoutAbstractMethod(name.to_string()),
Range::from_located(stmt),
));
}
}
}
}

View File

@@ -10,10 +10,10 @@ pub fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[With
if let Some(item) = items.first() {
let item_context = &item.context_expr;
if let ExprKind::Call { func, args, .. } = &item_context.node {
if match_name_or_attr(func, "assertRaises")
&& args.len() == 1
&& match_name_or_attr(args.first().unwrap(), "Exception")
if args.len() == 1
&& item.optional_vars.is_none()
&& match_name_or_attr(func, "assertRaises")
&& match_name_or_attr(args.first().unwrap(), "Exception")
{
checker.add_check(Check::new(
CheckKind::NoAssertRaisesException,

View File

@@ -1,22 +1,14 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::helpers::{compose_call_path, match_name_or_attr_from_module};
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path};
use crate::ast::types::{Range, ScopeKind};
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
fn is_cache_func(checker: &Checker, expr: &Expr) -> bool {
match_name_or_attr_from_module(
expr,
"lru_cache",
"functools",
checker.from_imports.get("functools"),
) || match_name_or_attr_from_module(
expr,
"cache",
"functools",
checker.from_imports.get("functools"),
)
let call_path = dealias_call_path(collect_call_paths(expr), &checker.import_aliases);
match_call_path(&call_path, "functools", "lru_cache", &checker.from_imports)
|| match_call_path(&call_path, "functools", "cache", &checker.from_imports)
}
/// B019
@@ -25,8 +17,8 @@ pub fn cached_instance_method(checker: &mut Checker, decorator_list: &[Expr]) {
for decorator in decorator_list {
// TODO(charlie): This should take into account `classmethod-decorators` and
// `staticmethod-decorators`.
if let Some(decorator_path) = compose_call_path(decorator) {
if decorator_path == "classmethod" || decorator_path == "staticmethod" {
if let ExprKind::Name { id, .. } = &decorator.node {
if id == "classmethod" || id == "staticmethod" {
return;
}
}

View File

@@ -21,20 +21,21 @@ fn type_pattern(elts: Vec<&Expr>) -> Expr {
)
}
pub fn duplicate_handler_exceptions(
fn duplicate_handler_exceptions<'a>(
checker: &mut Checker,
expr: &Expr,
elts: &[Expr],
) -> BTreeSet<String> {
let mut seen: BTreeSet<String> = Default::default();
let mut duplicates: BTreeSet<String> = Default::default();
expr: &'a Expr,
elts: &'a [Expr],
) -> BTreeSet<Vec<&'a str>> {
let mut seen: BTreeSet<Vec<&str>> = Default::default();
let mut duplicates: BTreeSet<Vec<&str>> = Default::default();
let mut unique_elts: Vec<&Expr> = Default::default();
for type_ in elts {
if let Some(name) = helpers::compose_call_path(type_) {
if seen.contains(&name) {
duplicates.insert(name);
let call_path = helpers::collect_call_paths(type_);
if !call_path.is_empty() {
if seen.contains(&call_path) {
duplicates.insert(call_path);
} else {
seen.insert(name);
seen.insert(call_path);
unique_elts.push(type_);
}
}
@@ -45,7 +46,11 @@ pub fn duplicate_handler_exceptions(
if !duplicates.is_empty() {
let mut check = Check::new(
CheckKind::DuplicateHandlerException(
duplicates.into_iter().sorted().collect::<Vec<String>>(),
duplicates
.into_iter()
.map(|call_path| call_path.join("."))
.sorted()
.collect::<Vec<String>>(),
),
Range::from_located(expr),
);
@@ -70,19 +75,20 @@ pub fn duplicate_handler_exceptions(
}
pub fn duplicate_exceptions(checker: &mut Checker, stmt: &Stmt, handlers: &[Excepthandler]) {
let mut seen: BTreeSet<String> = Default::default();
let mut duplicates: BTreeSet<String> = Default::default();
let mut seen: BTreeSet<Vec<&str>> = Default::default();
let mut duplicates: BTreeSet<Vec<&str>> = Default::default();
for handler in handlers {
match &handler.node {
ExcepthandlerKind::ExceptHandler { type_, .. } => {
if let Some(type_) = type_ {
match &type_.node {
ExprKind::Attribute { .. } | ExprKind::Name { .. } => {
if let Some(name) = helpers::compose_call_path(type_) {
if seen.contains(&name) {
duplicates.insert(name);
let call_path = helpers::collect_call_paths(type_);
if !call_path.is_empty() {
if seen.contains(&call_path) {
duplicates.insert(call_path);
} else {
seen.insert(name);
seen.insert(call_path);
}
}
}
@@ -105,7 +111,7 @@ pub fn duplicate_exceptions(checker: &mut Checker, stmt: &Stmt, handlers: &[Exce
if checker.settings.enabled.contains(&CheckCode::B025) {
for duplicate in duplicates.into_iter().sorted() {
checker.add_check(Check::new(
CheckKind::DuplicateTryBlockException(duplicate),
CheckKind::DuplicateTryBlockException(duplicate.join(".")),
Range::from_located(stmt),
));
}

View File

@@ -0,0 +1,19 @@
use rustpython_ast::{ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// B021
pub fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
if let Some(stmt) = body.first() {
if let StmtKind::Expr { value } = &stmt.node {
if let ExprKind::JoinedStr { .. } = value.node {
checker.add_check(Check::new(
CheckKind::FStringDocstring,
Range::from_located(stmt),
));
}
}
}
}

View File

@@ -1,6 +1,9 @@
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
use crate::ast::helpers::compose_call_path;
use crate::ast::helpers::{
collect_call_paths, compose_call_path, dealias_call_path, match_call_path,
};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
@@ -8,42 +11,54 @@ use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::flake8_bugbear::plugins::mutable_argument_default::is_mutable_func;
// TODO(charlie): Verify imports for each of the imported members.
const IMMUTABLE_FUNCS: [&str; 11] = [
"tuple",
"frozenset",
"operator.attrgetter",
"operator.itemgetter",
"operator.methodcaller",
"attrgetter",
"itemgetter",
"methodcaller",
"types.MappingProxyType",
"MappingProxyType",
"re.compile",
const IMMUTABLE_FUNCS: [(&str, &str); 7] = [
("", "tuple"),
("", "frozenset"),
("operator", "attrgetter"),
("operator", "itemgetter"),
("operator", "methodcaller"),
("types", "MappingProxyType"),
("re", "compile"),
];
fn is_immutable_func(expr: &Expr) -> bool {
compose_call_path(expr).map_or_else(|| false, |func| IMMUTABLE_FUNCS.contains(&func.as_str()))
fn is_immutable_func(
expr: &Expr,
extend_immutable_calls: &[(&str, &str)],
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
IMMUTABLE_FUNCS
.iter()
.chain(extend_immutable_calls)
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
}
struct ArgumentDefaultVisitor {
struct ArgumentDefaultVisitor<'a> {
checks: Vec<(CheckKind, Range)>,
extend_immutable_calls: &'a [(&'a str, &'a str)],
from_imports: &'a FnvHashMap<&'a str, FnvHashSet<&'a str>>,
import_aliases: &'a FnvHashMap<&'a str, &'a str>,
}
impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor
impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor<'b>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'a Expr) {
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::Call { func, args, .. } => {
if !is_mutable_func(func)
&& !is_immutable_func(func)
if !is_mutable_func(func, self.from_imports, self.import_aliases)
&& !is_immutable_func(
func,
self.extend_immutable_calls,
self.from_imports,
self.import_aliases,
)
&& !is_nan_or_infinity(func, args)
{
self.checks.push((
CheckKind::FunctionCallArgumentDefault,
CheckKind::FunctionCallArgumentDefault(compose_call_path(expr)),
Range::from_located(expr),
))
}
@@ -83,7 +98,27 @@ fn is_nan_or_infinity(expr: &Expr, args: &[Expr]) -> bool {
/// B008
pub fn function_call_argument_default(checker: &mut Checker, arguments: &Arguments) {
let mut visitor = ArgumentDefaultVisitor { checks: vec![] };
// Map immutable calls to (module, member) format.
let extend_immutable_cells: Vec<(&str, &str)> = checker
.settings
.flake8_bugbear
.extend_immutable_calls
.iter()
.map(|s| {
let s = s.as_str();
if let Some(index) = s.rfind('.') {
(&s[..index], &s[index + 1..])
} else {
("", s)
}
})
.collect();
let mut visitor = ArgumentDefaultVisitor {
checks: vec![],
extend_immutable_calls: &extend_immutable_cells,
from_imports: &checker.from_imports,
import_aliases: &checker.import_aliases,
};
for expr in arguments
.defaults
.iter()

View File

@@ -0,0 +1,49 @@
use rustpython_ast::{Stmt, StmtKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
fn walk_stmt(checker: &mut Checker, body: &[Stmt], f: fn(&Stmt) -> bool) {
for stmt in body {
if f(stmt) {
checker.add_check(Check::new(
CheckKind::JumpStatementInFinally(match &stmt.node {
StmtKind::Break { .. } => "break".to_string(),
StmtKind::Continue { .. } => "continue".to_string(),
StmtKind::Return { .. } => "return".to_string(),
_ => unreachable!(
"Expected StmtKind::Break | StmtKind::Continue | StmtKind::Return"
),
}),
Range::from_located(stmt),
));
}
match &stmt.node {
StmtKind::While { body, .. }
| StmtKind::For { body, .. }
| StmtKind::AsyncFor { body, .. } => {
walk_stmt(checker, body, |stmt| {
matches!(stmt.node, StmtKind::Return { .. })
});
}
StmtKind::If { body, .. }
| StmtKind::Try { body, .. }
| StmtKind::With { body, .. }
| StmtKind::AsyncWith { body, .. } => {
walk_stmt(checker, body, f);
}
_ => {}
}
}
}
/// B012
pub fn jump_statement_in_finally(checker: &mut Checker, finalbody: &[Stmt]) {
walk_stmt(checker, finalbody, |stmt| {
matches!(
stmt.node,
StmtKind::Break | StmtKind::Continue | StmtKind::Return { .. }
)
});
}

View File

@@ -0,0 +1,64 @@
use fnv::FnvHashMap;
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
#[derive(Default)]
struct NameFinder<'a> {
names: FnvHashMap<&'a str, &'a Expr>,
}
impl<'a, 'b> Visitor<'b> for NameFinder<'a>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::Name { id, .. } => {
self.names.insert(id, expr);
}
ExprKind::ListComp { generators, .. }
| ExprKind::DictComp { generators, .. }
| ExprKind::SetComp { generators, .. }
| ExprKind::GeneratorExp { generators, .. } => {
for comp in generators {
self.visit_expr(&comp.iter);
}
}
ExprKind::Lambda { args, body } => {
visitor::walk_expr(self, body);
for arg in args.args.iter() {
self.names.remove(arg.node.arg.as_str());
}
}
_ => visitor::walk_expr(self, expr),
}
}
}
/// B020
pub fn loop_variable_overrides_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) {
let target_names = {
let mut target_finder = NameFinder::default();
target_finder.visit_expr(target);
target_finder.names
};
let iter_names = {
let mut iter_finder = NameFinder::default();
iter_finder.visit_expr(iter);
iter_finder.names
};
for (name, expr) in target_names {
if iter_names.contains_key(name) {
checker.add_check(Check::new(
CheckKind::LoopVariableOverridesIterator(name.to_string()),
Range::from_located(expr),
));
}
}
}

View File

@@ -1,11 +1,15 @@
pub use abstract_base_class::abstract_base_class;
pub use assert_false::assert_false;
pub use assert_raises_exception::assert_raises_exception;
pub use assignment_to_os_environ::assignment_to_os_environ;
pub use cached_instance_method::cached_instance_method;
pub use cannot_raise_literal::cannot_raise_literal;
pub use duplicate_exceptions::{duplicate_exceptions, duplicate_handler_exceptions};
pub use duplicate_exceptions::duplicate_exceptions;
pub use f_string_docstring::f_string_docstring;
pub use function_call_argument_default::function_call_argument_default;
pub use getattr_with_constant::getattr_with_constant;
pub use jump_statement_in_finally::jump_statement_in_finally;
pub use loop_variable_overrides_iterator::loop_variable_overrides_iterator;
pub use mutable_argument_default::mutable_argument_default;
pub use redundant_tuple_in_exception_handler::redundant_tuple_in_exception_handler;
pub use setattr_with_constant::setattr_with_constant;
@@ -15,16 +19,21 @@ pub use unary_prefix_increment::unary_prefix_increment;
pub use unreliable_callable_check::unreliable_callable_check;
pub use unused_loop_control_variable::unused_loop_control_variable;
pub use useless_comparison::useless_comparison;
pub use useless_contextlib_suppress::useless_contextlib_suppress;
pub use useless_expression::useless_expression;
mod abstract_base_class;
mod assert_false;
mod assert_raises_exception;
mod assignment_to_os_environ;
mod cached_instance_method;
mod cannot_raise_literal;
mod duplicate_exceptions;
mod f_string_docstring;
mod function_call_argument_default;
mod getattr_with_constant;
mod jump_statement_in_finally;
mod loop_variable_overrides_iterator;
mod mutable_argument_default;
mod redundant_tuple_in_exception_handler;
mod setattr_with_constant;
@@ -34,4 +43,5 @@ mod unary_prefix_increment;
mod unreliable_callable_check;
mod unused_loop_control_variable;
mod useless_comparison;
mod useless_contextlib_suppress;
mod useless_expression;

View File

@@ -1,33 +1,30 @@
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Arguments, Expr, ExprKind};
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
// TODO(charlie): Verify imports for each of the imported members.
pub fn is_mutable_func(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Name { id, .. }
if id == "dict"
|| id == "list"
|| id == "set"
|| id == "Counter"
|| id == "OrderedDict"
|| id == "defaultdict"
|| id == "deque" =>
{
true
}
ExprKind::Attribute { value, attr, .. }
if (attr == "Counter"
|| attr == "OrderedDict"
|| attr == "defaultdict"
|| attr == "deque") =>
{
matches!(&value.node, ExprKind::Name { id, .. } if id == "collections")
}
_ => false,
}
const MUTABLE_FUNCS: [(&str, &str); 7] = [
("", "dict"),
("", "list"),
("", "set"),
("collections", "Counter"),
("collections", "OrderedDict"),
("collections", "defaultdict"),
("collections", "deque"),
];
pub fn is_mutable_func(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
MUTABLE_FUNCS
.iter()
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
}
/// B006
@@ -50,7 +47,7 @@ pub fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) {
));
}
ExprKind::Call { func, .. } => {
if is_mutable_func(func) {
if is_mutable_func(func, &checker.from_imports, &checker.import_aliases) {
checker.add_check(Check::new(
CheckKind::MutableArgumentDefault,
Range::from_located(expr),

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeMap;
use fnv::FnvHashMap;
use rustpython_ast::{Expr, ExprKind, Stmt};
use crate::ast::types::Range;
@@ -12,7 +11,7 @@ use crate::checks::{Check, CheckKind};
/// Identify all `ExprKind::Name` nodes in an AST.
struct NameFinder<'a> {
/// A map from identifier to defining expression.
names: BTreeMap<&'a str, &'a Expr>,
names: FnvHashMap<&'a str, &'a Expr>,
}
impl NameFinder<'_> {

View File

@@ -0,0 +1,22 @@
use rustpython_ast::Expr;
use crate::ast::helpers::{collect_call_paths, match_call_path};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// B005
pub fn useless_contextlib_suppress(checker: &mut Checker, expr: &Expr, args: &[Expr]) {
if match_call_path(
&collect_call_paths(expr),
"contextlib",
"suppress",
&checker.from_imports,
) && args.is_empty()
{
checker.add_check(Check::new(
CheckKind::UselessContextlibSuppress,
Range::from_located(expr),
));
}
}

View File

@@ -0,0 +1,22 @@
//! Settings for the `pep8-naming` plugin.
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
pub extend_immutable_calls: Option<Vec<String>>,
}
#[derive(Debug, Hash, Default)]
pub struct Settings {
pub extend_immutable_calls: Vec<String>,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
Self {
extend_immutable_calls: options.extend_immutable_calls.unwrap_or_default(),
}
}
}

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_bugbear/mod.rs
expression: checks
---
- kind:
FunctionCallArgumentDefault: Depends
location:
row: 19
column: 50
end_location:
row: 19
column: 63
fix: ~

View File

@@ -1,11 +1,11 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fs::File;
use std::io::{BufReader, Read};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use fnv::FnvHashSet;
use log::debug;
use path_absolutize::{path_dedot, Absolutize};
use walkdir::{DirEntry, WalkDir};
@@ -121,7 +121,7 @@ pub fn iter_python_files<'a>(
pub(crate) fn ignores_from_path<'a>(
path: &Path,
pattern_code_pairs: &'a [PerFileIgnore],
) -> Result<BTreeSet<&'a CheckCode>> {
) -> Result<FnvHashSet<&'a CheckCode>> {
let (file_path, file_basename) = extract_path_names(path)?;
Ok(pattern_code_pairs
.iter()

View File

@@ -1,9 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeSet;
use std::fs;
use std::path::PathBuf;
use once_cell::sync::Lazy;
use crate::python::sys::KNOWN_STANDARD_LIBRARY;
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone)]
@@ -31,8 +29,8 @@ pub fn categorize(
ImportType::ThirdParty
} else if extra_standard_library.contains(module_base) {
ImportType::StandardLibrary
} else if let Some(import_type) = STATIC_CLASSIFICATIONS.get(module_base) {
import_type.clone()
} else if module_base == "__future__" {
ImportType::Future
} else if KNOWN_STANDARD_LIBRARY.contains(module_base) {
ImportType::StandardLibrary
} else if find_local(src, module_base) {
@@ -42,14 +40,6 @@ pub fn categorize(
}
}
static STATIC_CLASSIFICATIONS: Lazy<BTreeMap<&'static str, ImportType>> = Lazy::new(|| {
BTreeMap::from([
("__future__", ImportType::Future),
// Relative imports (e.g., `from . import module`).
("", ImportType::FirstParty),
])
});
fn find_local(paths: &[PathBuf], base: &str) -> bool {
for path in paths {
if let Ok(metadata) = fs::metadata(path.join(base)) {

View File

@@ -1,6 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use fnv::FnvHashSet;
use itertools::Itertools;
use ropey::RopeBuilder;
use rustpython_ast::{Stmt, StmtKind};
@@ -36,15 +37,25 @@ fn normalize_imports<'a>(imports: &'a [&'a Stmt]) -> ImportBlock<'a> {
names,
level,
} => {
let targets = block
.import_from
.entry(ImportFromData { module, level })
.or_default();
for name in names {
targets.insert(AliasData {
name: &name.node.name,
asname: &name.node.asname,
});
if name.node.asname.is_none() {
block
.import_from
.entry(ImportFromData { module, level })
.or_default()
.insert(AliasData {
name: &name.node.name,
asname: &name.node.asname,
});
} else {
block.import_from_as.insert((
ImportFromData { module, level },
AliasData {
name: &name.node.name,
asname: &name.node.asname,
},
));
}
}
}
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
@@ -77,7 +88,7 @@ fn categorize_imports<'a>(
.import
.insert(alias);
}
// Categorize `StmtKind::ImportFrom`.
// Categorize `StmtKind::ImportFrom` (without re-export).
for (import_from, aliases) in block.import_from {
let classification = categorize(
&import_from.module_base(),
@@ -93,36 +104,74 @@ fn categorize_imports<'a>(
.import_from
.insert(import_from, aliases);
}
// Categorize `StmtKind::ImportFrom` (with re-export).
for (import_from, alias) in block.import_from_as {
let classification = categorize(
&import_from.module_base(),
import_from.level,
src,
known_first_party,
known_third_party,
extra_standard_library,
);
block_by_type
.entry(classification)
.or_default()
.import_from_as
.insert((import_from, alias));
}
block_by_type
}
fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
let mut ordered: OrderedImportBlock = Default::default();
// Sort `StmtKind::Import`.
for import in block
.import
.into_iter()
.sorted_by_cached_key(|alias| module_key(alias.name))
{
ordered.import.push(import);
}
ordered.import.extend(
block
.import
.into_iter()
.sorted_by_cached_key(|alias| module_key(alias.name, alias.asname)),
);
// Sort `StmtKind::ImportFrom`.
for (import_from, aliases) in
ordered.import_from.extend(
// Include all non-re-exports.
block
.import_from
.into_iter()
.sorted_by_cached_key(|(import_from, _)| {
import_from.module.as_ref().map(|module| module_key(module))
.chain(
// Include all re-exports.
block
.import_from_as
.into_iter()
.map(|(import_from, alias)| (import_from, FnvHashSet::from_iter([alias]))),
)
.map(|(import_from, aliases)| {
// Within each `StmtKind::ImportFrom`, sort the members.
(
import_from,
aliases
.into_iter()
.sorted_by_cached_key(|alias| member_key(alias.name, alias.asname))
.collect::<Vec<AliasData>>(),
)
})
{
ordered.import_from.push((
import_from,
aliases
.into_iter()
.sorted_by_cached_key(|alias| member_key(alias.name))
.collect(),
));
}
.sorted_by_cached_key(|(import_from, aliases)| {
// Sort each `StmtKind::ImportFrom` by module key, breaking ties based on
// members.
(
import_from
.module
.as_ref()
.map(|module| module_key(module, &None)),
aliases
.first()
.map(|alias| member_key(alias.name, alias.asname)),
)
}),
);
ordered
}
@@ -252,6 +301,7 @@ mod tests {
#[test_case(Path::new("separate_local_folder_imports.py"))]
#[test_case(Path::new("separate_third_party_imports.py"))]
#[test_case(Path::new("skip.py"))]
#[test_case(Path::new("sort_similar_imports.py"))]
#[test_case(Path::new("trailing_suffix.py"))]
fn isort(path: &Path) -> Result<()> {
let snapshot = format!("{}", path.to_string_lossy());

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 27
column: 0
fix:
patch:
content: "import A\nimport a\nimport B\nimport b\nimport x\nimport x as A\nimport x as Y\nimport x as a\nimport x as y\nfrom a import BAD as DEF\nfrom a import B, b\nfrom a import B as A\nfrom a import B as Abc\nfrom a import B as DEF\nfrom a import Boo as DEF\nfrom a import b as a\nfrom a import b as c\nfrom a import b as d\nfrom a import b as x\nfrom a import b as y\nfrom b import C, c\nfrom b import c as d\n"
location:
row: 1
column: 0
end_location:
row: 27
column: 0
applied: false

View File

@@ -8,16 +8,22 @@ pub enum Prefix {
Variables,
}
pub fn module_key(module_name: &str) -> String {
module_name.to_lowercase()
pub fn module_key<'a>(
name: &'a str,
asname: &'a Option<String>,
) -> (String, &'a str, &'a Option<String>) {
(name.to_lowercase(), name, asname)
}
pub fn member_key(member_name: &str) -> (Prefix, String) {
pub fn member_key<'a>(
name: &'a str,
asname: &'a Option<String>,
) -> (Prefix, String, &'a Option<String>) {
(
if member_name.len() > 1 && string::is_upper(member_name) {
if name.len() > 1 && string::is_upper(name) {
// Ex) `CONSTANT`
Prefix::Constants
} else if member_name
} else if name
.chars()
.next()
.map(|char| char.is_uppercase())
@@ -29,6 +35,7 @@ pub fn member_key(member_name: &str) -> (Prefix, String) {
// Ex) `variable`
Prefix::Variables
},
member_name.to_lowercase(),
name.to_lowercase(),
asname,
)
}

View File

@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet};
use fnv::{FnvHashMap, FnvHashSet};
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct ImportFromData<'a> {
@@ -48,16 +48,19 @@ impl Importable for ImportFromData<'_> {
#[derive(Debug, Default)]
pub struct ImportBlock<'a> {
// Map from (module, level) to `AliasData`.
pub import_from: BTreeMap<ImportFromData<'a>, BTreeSet<AliasData<'a>>>,
// Set of (name, asname).
pub import: BTreeSet<AliasData<'a>>,
// Set of (name, asname), used to track regular imports.
// Ex) `import module`
pub import: FnvHashSet<AliasData<'a>>,
// Map from (module, level) to `AliasData`, used to track 'from' imports.
// Ex) `from module import member`
pub import_from: FnvHashMap<ImportFromData<'a>, FnvHashSet<AliasData<'a>>>,
// Set of (module, level, name, asname), used to track re-exported 'from' imports.
// Ex) `from module import member as member`
pub import_from_as: FnvHashSet<(ImportFromData<'a>, AliasData<'a>)>,
}
#[derive(Debug, Default)]
pub struct OrderedImportBlock<'a> {
// Map from (module, level) to `AliasData`.
pub import_from: Vec<(ImportFromData<'a>, Vec<AliasData<'a>>)>,
// Set of (name, asname).
pub import: Vec<AliasData<'a>>,
pub import_from: Vec<(ImportFromData<'a>, Vec<AliasData<'a>>)>,
}

View File

@@ -29,7 +29,8 @@ mod directives;
mod docstrings;
mod flake8_2020;
pub mod flake8_annotations;
mod flake8_bugbear;
pub mod flake8_bandit;
pub mod flake8_bugbear;
mod flake8_builtins;
mod flake8_comprehensions;
mod flake8_print;
@@ -51,6 +52,8 @@ mod pyupgrade;
mod rules;
pub mod settings;
pub mod source_code_locator;
#[cfg(feature = "update-informer")]
pub mod updates;
pub mod visibility;
/// Run Ruff over Python source code directly.

View File

@@ -336,6 +336,7 @@ mod tests {
#[test_case(CheckCode::B009, Path::new("B009_B010.py"); "B009")]
#[test_case(CheckCode::B010, Path::new("B009_B010.py"); "B010")]
#[test_case(CheckCode::B011, Path::new("B011.py"); "B011")]
#[test_case(CheckCode::B012, Path::new("B012.py"); "B012")]
#[test_case(CheckCode::B013, Path::new("B013.py"); "B013")]
#[test_case(CheckCode::B014, Path::new("B014.py"); "B014")]
#[test_case(CheckCode::B015, Path::new("B015.py"); "B015")]
@@ -343,8 +344,13 @@ mod tests {
#[test_case(CheckCode::B017, Path::new("B017.py"); "B017")]
#[test_case(CheckCode::B018, Path::new("B018.py"); "B018")]
#[test_case(CheckCode::B019, Path::new("B019.py"); "B019")]
#[test_case(CheckCode::B020, Path::new("B020.py"); "B020")]
#[test_case(CheckCode::B021, Path::new("B021.py"); "B021")]
#[test_case(CheckCode::B022, Path::new("B022.py"); "B022")]
#[test_case(CheckCode::B024, Path::new("B024.py"); "B024")]
#[test_case(CheckCode::B025, Path::new("B025.py"); "B025")]
#[test_case(CheckCode::B026, Path::new("B026.py"); "B026")]
#[test_case(CheckCode::B027, Path::new("B027.py"); "B027")]
#[test_case(CheckCode::C400, Path::new("C400.py"); "C400")]
#[test_case(CheckCode::C401, Path::new("C401.py"); "C401")]
#[test_case(CheckCode::C402, Path::new("C402.py"); "C402")]
@@ -425,6 +431,7 @@ mod tests {
#[test_case(CheckCode::F401, Path::new("F401_3.py"); "F401_3")]
#[test_case(CheckCode::F401, Path::new("F401_4.py"); "F401_4")]
#[test_case(CheckCode::F401, Path::new("F401_5.py"); "F401_5")]
#[test_case(CheckCode::F401, Path::new("F401_6.py"); "F401_6")]
#[test_case(CheckCode::F402, Path::new("F402.py"); "F402")]
#[test_case(CheckCode::F403, Path::new("F403.py"); "F403")]
#[test_case(CheckCode::F404, Path::new("F404.py"); "F404")]
@@ -449,6 +456,8 @@ mod tests {
#[test_case(CheckCode::F821, Path::new("F821_1.py"); "F821_1")]
#[test_case(CheckCode::F821, Path::new("F821_2.py"); "F821_2")]
#[test_case(CheckCode::F821, Path::new("F821_3.py"); "F821_3")]
#[test_case(CheckCode::F821, Path::new("F821_4.py"); "F821_4")]
#[test_case(CheckCode::F821, Path::new("F821_5.py"); "F821_5")]
#[test_case(CheckCode::F822, Path::new("F822.py"); "F822")]
#[test_case(CheckCode::F823, Path::new("F823.py"); "F823")]
#[test_case(CheckCode::F831, Path::new("F831.py"); "F831")]
@@ -469,6 +478,12 @@ mod tests {
#[test_case(CheckCode::N816, Path::new("N816.py"); "N816")]
#[test_case(CheckCode::N817, Path::new("N817.py"); "N817")]
#[test_case(CheckCode::N818, Path::new("N818.py"); "N818")]
#[test_case(CheckCode::S101, Path::new("S101.py"); "S101")]
#[test_case(CheckCode::S102, Path::new("S102.py"); "S102")]
#[test_case(CheckCode::S104, Path::new("S104.py"); "S104")]
#[test_case(CheckCode::S105, Path::new("S105.py"); "S105")]
#[test_case(CheckCode::S106, Path::new("S106.py"); "S106")]
#[test_case(CheckCode::S107, Path::new("S107.py"); "S107")]
#[test_case(CheckCode::T201, Path::new("T201.py"); "T201")]
#[test_case(CheckCode::T203, Path::new("T203.py"); "T203")]
#[test_case(CheckCode::U001, Path::new("U001.py"); "U001")]
@@ -509,7 +524,7 @@ mod tests {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(
Path::new("./resources/test/fixtures").join(path).as_path(),
&settings::Settings::for_rule(check_code.clone()),
&settings::Settings::for_rule(check_code),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);

View File

@@ -17,6 +17,8 @@ use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::types::FilePattern;
use ::ruff::settings::user::UserConfiguration;
use ::ruff::settings::{pyproject, Settings};
#[cfg(feature = "update-informer")]
use ::ruff::updates;
use anyhow::Result;
use clap::Parser;
use colored::Colorize;
@@ -26,11 +28,6 @@ use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use walkdir::DirEntry;
#[cfg(feature = "update-informer")]
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
#[cfg(feature = "update-informer")]
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Shim that calls par_iter except for wasm because there's no wasm support in
/// rayon yet (there is a shim to be used for the web, but it requires js
/// cooperation) Unfortunately, ParallelIterator does not implement Iterator so
@@ -45,30 +42,6 @@ fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl Iterator<Item = &T> {
iterable.iter()
}
#[cfg(feature = "update-informer")]
fn check_for_updates() {
use update_informer::{registry, Check};
let informer = update_informer::new(registry::PyPI, CARGO_PKG_NAME, CARGO_PKG_VERSION);
if let Some(new_version) = informer.check_version().ok().flatten() {
let msg = format!(
"A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",
pkg_name = CARGO_PKG_NAME.italic().cyan(),
pkg_version = CARGO_PKG_VERSION,
new_version = new_version.to_string().green()
);
let cmd = format!(
"Run to update: {cmd} {pkg_name}",
cmd = "pip3 install --upgrade".green(),
pkg_name = CARGO_PKG_NAME.green()
);
println!("\n{msg}\n{cmd}");
}
}
fn show_settings(
configuration: Configuration,
project_root: Option<PathBuf>,
@@ -292,6 +265,9 @@ fn inner_main() -> Result<ExitCode> {
if !cli.extend_ignore.is_empty() {
configuration.extend_ignore = cli.extend_ignore;
}
if let Some(line_length) = cli.line_length {
configuration.line_length = line_length;
}
if let Some(target_version) = cli.target_version {
configuration.target_version = target_version;
}
@@ -402,8 +378,8 @@ fn inner_main() -> Result<ExitCode> {
// Check for updates if we're in a non-silent log level.
#[cfg(feature = "update-informer")]
if !is_stdin && log_level >= LogLevel::Default {
check_for_updates();
if !is_stdin && log_level >= LogLevel::Default && atty::is(atty::Stream::Stdout) {
let _ = updates::check_for_updates();
}
if messages.iter().any(|message| !message.fixed) && !cli.exit_zero {

View File

@@ -10,7 +10,7 @@ use regex::Regex;
use crate::checks::{Check, CheckCode};
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?P<noqa>\s*# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)")
Regex::new(r"(?P<noqa>\s*(?i:# noqa)(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)")
.expect("Invalid regex")
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").expect("Invalid regex"));
@@ -118,7 +118,21 @@ mod tests {
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::noqa::add_noqa_inner;
use crate::noqa::{add_noqa_inner, NO_QA_REGEX};
#[test]
fn regex() {
assert!(NO_QA_REGEX.is_match("# noqa"));
assert!(NO_QA_REGEX.is_match("# NoQA"));
assert!(NO_QA_REGEX.is_match("# noqa: F401"));
assert!(NO_QA_REGEX.is_match("# NoQA: F401"));
assert!(NO_QA_REGEX.is_match("# noqa: F401, E501"));
assert!(NO_QA_REGEX.is_match("# noqa:F401"));
assert!(NO_QA_REGEX.is_match("# NoQA:F401"));
assert!(NO_QA_REGEX.is_match("# noqa:F401, E501"));
}
#[test]
fn modification() -> Result<()> {

View File

@@ -1,11 +1,11 @@
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use crate::ast::types::{FunctionScope, Range, Scope, ScopeKind};
use crate::ast::types::{Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pep8_naming::helpers;
use crate::pep8_naming::helpers::FunctionType;
use crate::pep8_naming::settings::Settings;
use crate::python::string;
use crate::python::string::{self};
/// N801
pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option<Check> {
@@ -100,20 +100,6 @@ pub fn invalid_first_argument_name_for_method(
None
}
/// N806
pub fn non_lowercase_variable_in_function(scope: &Scope, expr: &Expr, name: &str) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Function(FunctionScope { .. })) {
return None;
}
if name.to_lowercase() != name {
return Some(Check::new(
CheckKind::NonLowercaseVariableInFunction(name.to_string()),
Range::from_located(expr),
));
}
None
}
/// N807
pub fn dunder_function_name(scope: &Scope, stmt: &Stmt, name: &str) -> Option<Check> {
if matches!(scope.kind, ScopeKind::Class(_)) {
@@ -192,38 +178,6 @@ pub fn camelcase_imported_as_constant(
None
}
/// N815
pub fn mixed_case_variable_in_class_scope(scope: &Scope, expr: &Expr, name: &str) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Class(_)) {
return None;
}
if helpers::is_mixed_case(name) {
return Some(Check::new(
CheckKind::MixedCaseVariableInClassScope(name.to_string()),
Range::from_located(expr),
));
}
None
}
/// N816
pub fn mixed_case_variable_in_global_scope(
scope: &Scope,
expr: &Expr,
name: &str,
) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Module) {
return None;
}
if helpers::is_mixed_case(name) {
return Some(Check::new(
CheckKind::MixedCaseVariableInGlobalScope(name.to_string()),
Range::from_located(expr),
));
}
None
}
/// N817
pub fn camelcase_imported_as_acronym(
import_from: &Stmt,

View File

@@ -1,7 +1,8 @@
use fnv::{FnvHashMap, FnvHashSet};
use itertools::Itertools;
use rustpython_ast::{Expr, ExprKind};
use rustpython_ast::{Expr, ExprKind, Stmt, StmtKind};
use crate::ast::helpers::match_name_or_attr;
use crate::ast::helpers::{collect_call_paths, match_call_path, match_name_or_attr};
use crate::ast::types::{Scope, ScopeKind};
use crate::pep8_naming::settings::Settings;
use crate::python::string::{is_lower, is_upper};
@@ -78,12 +79,28 @@ pub fn is_acronym(name: &str, asname: &str) -> bool {
name.chars().filter(|c| c.is_uppercase()).join("") == asname
}
pub fn is_namedtuple_assignment(
stmt: &Stmt,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> bool {
if let StmtKind::Assign { value, .. } = &stmt.node {
match_call_path(
&collect_call_paths(value),
"collections",
"namedtuple",
from_imports,
)
} else {
false
}
}
#[cfg(test)]
mod tests {
use crate::pep8_naming::helpers::{is_acronym, is_camelcase, is_mixed_case};
#[test]
fn test_is_camelcase() -> () {
fn test_is_camelcase() {
assert!(is_camelcase("Camel"));
assert!(is_camelcase("CamelCase"));
assert!(!is_camelcase("camel"));
@@ -93,7 +110,7 @@ mod tests {
}
#[test]
fn test_is_mixed_case() -> () {
fn test_is_mixed_case() {
assert!(is_mixed_case("mixedCase"));
assert!(is_mixed_case("mixed_Case"));
assert!(is_mixed_case("_mixed_Case"));
@@ -104,7 +121,7 @@ mod tests {
}
#[test]
fn test_is_acronym() -> () {
fn test_is_acronym() {
assert!(is_acronym("AB", "AB"));
assert!(is_acronym("AbcDef", "AD"));
assert!(!is_acronym("AbcDef", "Ad"));

View File

@@ -1,3 +1,4 @@
pub mod checks;
mod helpers;
pub mod plugins;
pub mod settings;

View File

@@ -0,0 +1,58 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::CheckKind;
use crate::pep8_naming::helpers;
use crate::Check;
/// N806
pub fn non_lowercase_variable_in_function(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
) {
if name.to_lowercase() != name
&& !helpers::is_namedtuple_assignment(stmt, &checker.from_imports)
{
checker.add_check(Check::new(
CheckKind::NonLowercaseVariableInFunction(name.to_string()),
Range::from_located(expr),
));
}
}
/// N815
pub fn mixed_case_variable_in_class_scope(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
) {
if helpers::is_mixed_case(name)
&& !helpers::is_namedtuple_assignment(stmt, &checker.from_imports)
{
checker.add_check(Check::new(
CheckKind::MixedCaseVariableInClassScope(name.to_string()),
Range::from_located(expr),
));
}
}
/// N816
pub fn mixed_case_variable_in_global_scope(
checker: &mut Checker,
expr: &Expr,
stmt: &Stmt,
name: &str,
) {
if helpers::is_mixed_case(name)
&& !helpers::is_namedtuple_assignment(stmt, &checker.from_imports)
{
checker.add_check(Check::new(
CheckKind::MixedCaseVariableInGlobalScope(name.to_string()),
Range::from_located(expr),
));
}
}

View File

@@ -1,5 +1,6 @@
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
@@ -696,7 +697,7 @@ pub fn ends_with_period(checker: &mut Checker, definition: &Definition) {
..
} = &docstring.node
{
if let Some(string) = string.lines().next() {
if let Some(string) = string.trim().lines().next() {
if !string.ends_with('.') {
checker.add_check(Check::new(
CheckKind::EndsInPeriod,
@@ -806,7 +807,7 @@ pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) {
..
} = &docstring.node
{
if let Some(string) = string.lines().next() {
if let Some(string) = string.trim().lines().next() {
if !(string.ends_with('.') || string.ends_with('!') || string.ends_with('?')) {
checker.add_check(Check::new(
CheckKind::EndsInPunctuation,
@@ -1287,7 +1288,11 @@ fn common_section(
blanks_and_section_underline(checker, definition, context);
}
fn missing_args(checker: &mut Checker, definition: &Definition, docstrings_args: &BTreeSet<&str>) {
fn missing_args(
checker: &mut Checker,
definition: &Definition,
docstrings_args: &FnvHashSet<&str>,
) {
if let DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent)
| DefinitionKind::Method(parent) = definition.kind
@@ -1377,7 +1382,7 @@ fn args_section(checker: &mut Checker, definition: &Definition, context: &Sectio
checker,
definition,
// Collect the list of arguments documented in the docstring.
&BTreeSet::from_iter(args_sections.iter().filter_map(|section| {
&FnvHashSet::from_iter(args_sections.iter().filter_map(|section| {
match GOOGLE_ARGS_REGEX.captures(section.as_str()) {
Some(caps) => caps.get(1).map(|arg_name| arg_name.as_str()),
None => None,
@@ -1388,7 +1393,7 @@ fn args_section(checker: &mut Checker, definition: &Definition, context: &Sectio
fn parameters_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) {
// Collect the list of arguments documented in the docstring.
let mut docstring_args: BTreeSet<&str> = Default::default();
let mut docstring_args: FnvHashSet<&str> = FnvHashSet::default();
let section_level_indent = helpers::leading_space(context.line);
for i in 1..context.following_lines.len() {
let current_line = context.following_lines[i - 1];

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use regex::Regex;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind,
@@ -113,7 +112,7 @@ pub fn duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
}
// Search for duplicates.
let mut idents: BTreeSet<&str> = BTreeSet::new();
let mut idents: FnvHashSet<&str> = FnvHashSet::default();
for arg in all_arguments {
let ident = &arg.node.arg;
if idents.contains(ident.as_str()) {

View File

@@ -27,7 +27,7 @@ mod tests {
use crate::python::string::{is_lower, is_upper};
#[test]
fn test_is_lower() -> () {
fn test_is_lower() {
assert!(is_lower("abc"));
assert!(is_lower("a_b_c"));
assert!(is_lower("a2c"));
@@ -38,7 +38,7 @@ mod tests {
}
#[test]
fn test_is_upper() -> () {
fn test_is_upper() {
assert!(is_upper("ABC"));
assert!(is_upper("A_B_C"));
assert!(is_upper("A2C"));

View File

@@ -1,10 +1,9 @@
use std::collections::BTreeSet;
use fnv::FnvHashSet;
use once_cell::sync::Lazy;
// See: https://pycqa.github.io/isort/docs/configuration/options.html#known-standard-library
pub static KNOWN_STANDARD_LIBRARY: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
pub static KNOWN_STANDARD_LIBRARY: Lazy<FnvHashSet<&'static str>> = Lazy::new(|| {
FnvHashSet::from_iter([
"_ast",
"_dummy_thread",
"_thread",

View File

@@ -1,11 +1,12 @@
use std::collections::{BTreeMap, BTreeSet};
use fnv::{FnvHashMap, FnvHashSet};
use once_cell::sync::Lazy;
use rustpython_ast::{Expr, ExprKind};
use rustpython_ast::Expr;
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path};
// See: https://pypi.org/project/typing-extensions/
static TYPING_EXTENSIONS: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
static TYPING_EXTENSIONS: Lazy<FnvHashSet<&'static str>> = Lazy::new(|| {
FnvHashSet::from_iter([
"Annotated",
"Any",
"AsyncContextManager",
@@ -64,144 +65,144 @@ pub fn in_extensions(name: &str) -> bool {
}
// See: https://docs.python.org/3/library/typing.html
static IMPORTED_SUBSCRIPTS: Lazy<BTreeMap<&'static str, BTreeSet<&'static str>>> =
Lazy::new(|| {
let mut import_map = BTreeMap::new();
for (name, module) in [
// `collections`
("ChainMap", "collections"),
("Counter", "collections"),
("OrderedDict", "collections"),
("defaultdict", "collections"),
("deque", "collections"),
// `collections.abc`
("AsyncGenerator", "collections.abc"),
("AsyncIterable", "collections.abc"),
("AsyncIterator", "collections.abc"),
("Awaitable", "collections.abc"),
("ByteString", "collections.abc"),
("Callable", "collections.abc"),
("Collection", "collections.abc"),
("Container", "collections.abc"),
("Coroutine", "collections.abc"),
("Generator", "collections.abc"),
("ItemsView", "collections.abc"),
("Iterable", "collections.abc"),
("Iterator", "collections.abc"),
("KeysView", "collections.abc"),
("Mapping", "collections.abc"),
("MappingView", "collections.abc"),
("MutableMapping", "collections.abc"),
("MutableSequence", "collections.abc"),
("MutableSet", "collections.abc"),
("Reversible", "collections.abc"),
("Sequence", "collections.abc"),
("Set", "collections.abc"),
("ValuesView", "collections.abc"),
// `contextlib`
("AbstractAsyncContextManager", "contextlib"),
("AbstractContextManager", "contextlib"),
// `re`
("Match", "re"),
("Pattern", "re"),
// `typing`
("AbstractSet", "typing"),
("Annotated", "typing"),
("AsyncContextManager", "typing"),
("AsyncGenerator", "typing"),
("AsyncIterator", "typing"),
("Awaitable", "typing"),
("BinaryIO", "typing"),
("ByteString", "typing"),
("Callable", "typing"),
("ChainMap", "typing"),
("ClassVar", "typing"),
("Collection", "typing"),
("Concatenate", "typing"),
("Container", "typing"),
("ContextManager", "typing"),
("Coroutine", "typing"),
("Counter", "typing"),
("DefaultDict", "typing"),
("Deque", "typing"),
("Dict", "typing"),
("Final", "typing"),
("FrozenSet", "typing"),
("Generator", "typing"),
("Generic", "typing"),
("IO", "typing"),
("ItemsView", "typing"),
("Iterable", "typing"),
("Iterator", "typing"),
("KeysView", "typing"),
("List", "typing"),
("Mapping", "typing"),
("Match", "typing"),
("MutableMapping", "typing"),
("MutableSequence", "typing"),
("MutableSet", "typing"),
("Optional", "typing"),
("OrderedDict", "typing"),
("Pattern", "typing"),
("Reversible", "typing"),
("Sequence", "typing"),
("Set", "typing"),
("TextIO", "typing"),
("Tuple", "typing"),
("Type", "typing"),
("TypeGuard", "typing"),
("Union", "typing"),
("Unpack", "typing"),
("ValuesView", "typing"),
// `typing.io`
("BinaryIO", "typing.io"),
("IO", "typing.io"),
("TextIO", "typing.io"),
// `typing.re`
("Match", "typing.re"),
("Pattern", "typing.re"),
// `typing_extensions`
("Annotated", "typing_extensions"),
("AsyncContextManager", "typing_extensions"),
("AsyncGenerator", "typing_extensions"),
("AsyncIterable", "typing_extensions"),
("AsyncIterator", "typing_extensions"),
("Awaitable", "typing_extensions"),
("ChainMap", "typing_extensions"),
("ClassVar", "typing_extensions"),
("Concatenate", "typing_extensions"),
("ContextManager", "typing_extensions"),
("Coroutine", "typing_extensions"),
("Counter", "typing_extensions"),
("DefaultDict", "typing_extensions"),
("Deque", "typing_extensions"),
("Type", "typing_extensions"),
// `weakref`
("WeakKeyDictionary", "weakref"),
("WeakSet", "weakref"),
("WeakValueDictionary", "weakref"),
] {
import_map
.entry(name)
.or_insert_with(BTreeSet::new)
.insert(module);
}
import_map
});
const SUBSCRIPTS: &[(&str, &str)] = &[
// builtins
("", "dict"),
("", "frozenset"),
("", "list"),
("", "set"),
("", "tuple"),
("", "type"),
// `collections`
("collections", "ChainMap"),
("collections", "Counter"),
("collections", "OrderedDict"),
("collections", "defaultdict"),
("collections", "deque"),
// `collections.abc`
("collections.abc", "AsyncGenerator"),
("collections.abc", "AsyncIterable"),
("collections.abc", "AsyncIterator"),
("collections.abc", "Awaitable"),
("collections.abc", "ByteString"),
("collections.abc", "Callable"),
("collections.abc", "Collection"),
("collections.abc", "Container"),
("collections.abc", "Coroutine"),
("collections.abc", "Generator"),
("collections.abc", "ItemsView"),
("collections.abc", "Iterable"),
("collections.abc", "Iterator"),
("collections.abc", "KeysView"),
("collections.abc", "Mapping"),
("collections.abc", "MappingView"),
("collections.abc", "MutableMapping"),
("collections.abc", "MutableSequence"),
("collections.abc", "MutableSet"),
("collections.abc", "Reversible"),
("collections.abc", "Sequence"),
("collections.abc", "Set"),
("collections.abc", "ValuesView"),
// `contextlib`
("contextlib", "AbstractAsyncContextManager"),
("contextlib", "AbstractContextManager"),
// `re`
("re", "Match"),
("re", "Pattern"),
// `typing`
("typing", "AbstractSet"),
("typing", "AsyncContextManager"),
("typing", "AsyncGenerator"),
("typing", "AsyncIterator"),
("typing", "Awaitable"),
("typing", "BinaryIO"),
("typing", "ByteString"),
("typing", "Callable"),
("typing", "ChainMap"),
("typing", "ClassVar"),
("typing", "Collection"),
("typing", "Concatenate"),
("typing", "Container"),
("typing", "ContextManager"),
("typing", "Coroutine"),
("typing", "Counter"),
("typing", "DefaultDict"),
("typing", "Deque"),
("typing", "Dict"),
("typing", "Final"),
("typing", "FrozenSet"),
("typing", "Generator"),
("typing", "Generic"),
("typing", "IO"),
("typing", "ItemsView"),
("typing", "Iterable"),
("typing", "Iterator"),
("typing", "KeysView"),
("typing", "List"),
("typing", "Mapping"),
("typing", "Match"),
("typing", "MutableMapping"),
("typing", "MutableSequence"),
("typing", "MutableSet"),
("typing", "Optional"),
("typing", "OrderedDict"),
("typing", "Pattern"),
("typing", "Reversible"),
("typing", "Sequence"),
("typing", "Set"),
("typing", "TextIO"),
("typing", "Tuple"),
("typing", "Type"),
("typing", "TypeGuard"),
("typing", "Union"),
("typing", "Unpack"),
("typing", "ValuesView"),
// `typing.io`
("typing.io", "BinaryIO"),
("typing.io", "IO"),
("typing.io", "TextIO"),
// `typing.re`
("typing.re", "Match"),
("typing.re", "Pattern"),
// `typing_extensions`
("typing_extensions", "AsyncContextManager"),
("typing_extensions", "AsyncGenerator"),
("typing_extensions", "AsyncIterable"),
("typing_extensions", "AsyncIterator"),
("typing_extensions", "Awaitable"),
("typing_extensions", "ChainMap"),
("typing_extensions", "ClassVar"),
("typing_extensions", "Concatenate"),
("typing_extensions", "ContextManager"),
("typing_extensions", "Coroutine"),
("typing_extensions", "Counter"),
("typing_extensions", "DefaultDict"),
("typing_extensions", "Deque"),
("typing_extensions", "Type"),
// `weakref`
("weakref", "WeakKeyDictionary"),
("weakref", "WeakSet"),
("weakref", "WeakValueDictionary"),
];
// See: https://docs.python.org/3/library/typing.html
const PEP_583_SUBSCRIPTS: &[(&str, &str)] = &[
// `typing`
("typing", "Annotated"),
// `typing_extensions`
("typing_extensions", "Annotated"),
];
// These are all assumed to come from the `typing` module.
// See: https://peps.python.org/pep-0585/
static PEP_585_BUILTINS_ELIGIBLE: Lazy<BTreeSet<&'static str>> =
Lazy::new(|| BTreeSet::from(["Dict", "FrozenSet", "List", "Set", "Tuple", "Type"]));
// These are all assumed to come from the `typing` module.
// See: https://peps.python.org/pep-0585/
static PEP_585_BUILTINS: Lazy<BTreeSet<&'static str>> =
Lazy::new(|| BTreeSet::from(["dict", "frozenset", "list", "set", "tuple", "type"]));
fn is_pep593_annotated_subscript(name: &str) -> bool {
name == "Annotated"
}
const PEP_585_BUILTINS_ELIGIBLE: &[(&str, &str)] = &[
("typing", "Dict"),
("typing", "FrozenSet"),
("typing", "List"),
("typing", "Set"),
("typing", "Tuple"),
("typing", "Type"),
("typing_extensions", "Type"),
];
pub enum SubscriptKind {
AnnotatedSubscript,
@@ -210,72 +211,39 @@ pub enum SubscriptKind {
pub fn match_annotated_subscript(
expr: &Expr,
imports: &BTreeMap<&str, BTreeSet<&str>>,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> Option<SubscriptKind> {
match &expr.node {
ExprKind::Attribute { attr, value, .. } => {
if let ExprKind::Name { id, .. } = &value.node {
// If `id` is `typing` and `attr` is `Union`, verify that `typing.Union` is an
// annotated subscript.
if IMPORTED_SUBSCRIPTS
.get(&attr.as_str())
.map(|imports| imports.contains(&id.as_str()))
.unwrap_or_default()
{
return if is_pep593_annotated_subscript(attr) {
Some(SubscriptKind::PEP593AnnotatedSubscript)
} else {
Some(SubscriptKind::AnnotatedSubscript)
};
}
}
}
ExprKind::Name { id, .. } => {
// Built-ins (no import necessary).
if PEP_585_BUILTINS.contains(&id.as_str()) {
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
if !call_path.is_empty() {
for (module, member) in SUBSCRIPTS {
if match_call_path(&call_path, module, member, from_imports) {
return Some(SubscriptKind::AnnotatedSubscript);
}
// Verify that, e.g., `Union` is a reference to `typing.Union`.
if let Some(modules) = IMPORTED_SUBSCRIPTS.get(&id.as_str()) {
for module in modules {
if imports
.get(module)
.map(|imports| imports.contains(&id.as_str()))
.unwrap_or_default()
{
return if is_pep593_annotated_subscript(id) {
Some(SubscriptKind::PEP593AnnotatedSubscript)
} else {
Some(SubscriptKind::AnnotatedSubscript)
};
}
}
}
for (module, member) in PEP_583_SUBSCRIPTS {
if match_call_path(&call_path, module, member, from_imports) {
return Some(SubscriptKind::PEP593AnnotatedSubscript);
}
}
_ => {}
}
None
}
/// Returns `true` if `Expr` represents a reference to a typing object with a
/// PEP 585 built-in. Note that none of the PEP 585 built-ins are in
/// `typing_extensions`.
pub fn is_pep585_builtin(expr: &Expr, typing_imports: Option<&BTreeSet<&str>>) -> bool {
match &expr.node {
ExprKind::Attribute { attr, value, .. } => {
if let ExprKind::Name { id, .. } = &value.node {
id == "typing" && PEP_585_BUILTINS_ELIGIBLE.contains(&attr.as_str())
} else {
false
/// PEP 585 built-in.
pub fn is_pep585_builtin(
expr: &Expr,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> bool {
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
if !call_path.is_empty() {
for (module, member) in PEP_585_BUILTINS_ELIGIBLE {
if match_call_path(&call_path, module, member, from_imports) {
return true;
}
}
ExprKind::Name { id, .. } => {
typing_imports
.map(|imports| imports.contains(&id.as_str()))
.unwrap_or_default()
&& PEP_585_BUILTINS_ELIGIBLE.contains(&id.as_str())
}
_ => false,
}
false
}

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeSet;
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Constant, KeywordData};
use rustpython_parser::ast::{ArgData, Expr, ExprKind, Stmt, StmtKind};
@@ -164,7 +163,8 @@ pub fn type_of_primitive(func: &Expr, args: &[Expr], location: Range) -> Option<
pub fn unnecessary_lru_cache_params(
decorator_list: &[Expr],
target_version: PythonVersion,
imports: Option<&BTreeSet<&str>>,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
) -> Option<Check> {
for expr in decorator_list.iter() {
if let ExprKind::Call {
@@ -174,7 +174,13 @@ pub fn unnecessary_lru_cache_params(
} = &expr.node
{
if args.is_empty()
&& helpers::match_name_or_attr_from_module(func, "lru_cache", "functools", imports)
&& helpers::match_module_member(
func,
"functools",
"lru_cache",
from_imports,
import_aliases,
)
{
// Ex) `functools.lru_cache()`
if keywords.is_empty() {

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeMap;
use fnv::FnvHashMap;
use once_cell::sync::Lazy;
use rustpython_ast::{Expr, ExprKind};
@@ -8,8 +7,8 @@ use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
static DEPRECATED_ALIASES: Lazy<BTreeMap<&'static str, &'static str>> = Lazy::new(|| {
BTreeMap::from([
static DEPRECATED_ALIASES: Lazy<FnvHashMap<&'static str, &'static str>> = Lazy::new(|| {
FnvHashMap::from_iter([
("failUnlessEqual", "assertEqual"),
("assertEquals", "assertEqual"),
("failIfEqual", "assertNotEqual"),

View File

@@ -8,7 +8,8 @@ pub fn unnecessary_lru_cache_params(checker: &mut Checker, decorator_list: &[Exp
if let Some(mut check) = checks::unnecessary_lru_cache_params(
decorator_list,
checker.settings.target_version,
checker.from_imports.get("functools"),
&checker.from_imports,
&checker.import_aliases,
) {
if checker.patch() {
if let Some(fix) =

View File

@@ -7,13 +7,14 @@ use crate::checks::{Check, CheckKind};
/// U006
pub fn use_pep585_annotation(checker: &mut Checker, expr: &Expr, id: &str) {
let replacement = checker.import_aliases.get(id).unwrap_or(&id);
let mut check = Check::new(
CheckKind::UsePEP585Annotation(id.to_string()),
CheckKind::UsePEP585Annotation(replacement.to_string()),
Range::from_located(expr),
);
if checker.patch() {
check.amend(Fix::replacement(
id.to_lowercase(),
replacement.to_lowercase(),
expr.location,
expr.end_location.unwrap(),
));

View File

@@ -1,5 +1,6 @@
use rustpython_ast::{Constant, Expr, ExprKind, Operator};
use crate::ast::helpers::{collect_call_paths, dealias_call_path};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::check_ast::Checker;
@@ -43,7 +44,8 @@ fn union(elts: &[Expr]) -> Expr {
/// U007
pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, slice: &Expr) {
if checker.match_typing_module(value, "Optional") {
let call_path = dealias_call_path(collect_call_paths(value), &checker.import_aliases);
if checker.match_typing_call_path(&call_path, "Optional") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if checker.patch() {
let mut generator = SourceGenerator::new();
@@ -58,7 +60,7 @@ pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, s
}
}
checker.add_check(check);
} else if checker.match_typing_module(value, "Union") {
} else if checker.match_typing_call_path(&call_path, "Union") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if checker.patch() {
match &slice.node {

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeMap;
use fnv::FnvHashMap;
use once_cell::sync::Lazy;
use rustpython_ast::Location;
@@ -10,8 +9,8 @@ use crate::source_code_locator::SourceCodeLocator;
use crate::Check;
/// See: https://github.com/microsoft/vscode/blob/095ddabc52b82498ee7f718a34f9dd11d59099a8/src/vs/base/common/strings.ts#L1094
static CONFUSABLES: Lazy<BTreeMap<u32, u32>> = Lazy::new(|| {
BTreeMap::from([
static CONFUSABLES: Lazy<FnvHashMap<u32, u32>> = Lazy::new(|| {
FnvHashMap::from_iter([
(8232, 32),
(8233, 32),
(5760, 32),

View File

@@ -12,7 +12,7 @@ use regex::Regex;
use crate::checks_gen::CheckCodePrefix;
use crate::settings::pyproject::load_options;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
use crate::{flake8_annotations, flake8_quotes, fs, isort, pep8_naming};
use crate::{flake8_annotations, flake8_bugbear, flake8_quotes, fs, isort, pep8_naming};
#[derive(Debug)]
pub struct Configuration {
@@ -30,6 +30,7 @@ pub struct Configuration {
pub target_version: PythonVersion,
// Plugins
pub flake8_annotations: flake8_annotations::settings::Settings,
pub flake8_bugbear: flake8_bugbear::settings::Settings,
pub flake8_quotes: flake8_quotes::settings::Settings,
pub isort: isort::settings::Settings,
pub pep8_naming: pep8_naming::settings::Settings,
@@ -133,6 +134,10 @@ impl Configuration {
.flake8_annotations
.map(flake8_annotations::settings::Settings::from_options)
.unwrap_or_default(),
flake8_bugbear: options
.flake8_bugbear
.map(flake8_bugbear::settings::Settings::from_options)
.unwrap_or_default(),
flake8_quotes: options
.flake8_quotes
.map(flake8_quotes::settings::Settings::from_options)

Some files were not shown because too many files have changed in this diff Show More