Compare commits

...

53 Commits

Author SHA1 Message Date
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
Charlie Marsh
da9ae6a42a Bump version to 0.0.114 2022-11-12 11:55:18 -05:00
Martin Lehoux
afa59d78bb feat: no unnecessary encode utf8 (#686) 2022-11-12 11:54:36 -05:00
Charlie Marsh
bbc38fea73 Avoid generating empty statement bodies (#700) 2022-11-12 11:39:09 -05:00
Chammika Mannakkara
6bcc11a90f add fixes for __future__ import removal (#682) 2022-11-12 11:28:05 -05:00
Harutaka Kawamura
6f36e5dd25 Implement B019 (#695) 2022-11-12 11:14:03 -05:00
Anders Kaseorg
1d13752eb1 Remove static isort classifications for __main__, disutils (#694) 2022-11-12 09:13:38 -05:00
Anders Kaseorg
394af0dcff Disable default features of chrono (#696) 2022-11-12 09:02:02 -05:00
Charlie Marsh
51cee471a0 Add test case for import-from wrapping 2022-11-11 23:46:19 -05:00
Charlie Marsh
8df3a5437a Take indentation into account for import-from wrapping (#693) 2022-11-11 23:45:04 -05:00
Charlie Marsh
a21fe716f2 Bump version to 0.0.113 2022-11-11 22:42:02 -05:00
Charlie Marsh
558883299a Default to isort's import sort logic (#691) 2022-11-11 22:41:39 -05:00
Charlie Marsh
048a13c795 Add a separate local folder category for imports (#690) 2022-11-11 22:12:48 -05:00
Anders Kaseorg
5a8b7c1d20 Implement flake8-2020 (sys.version, sys.version_info misuse) (#688) 2022-11-11 20:39:37 -05:00
Charlie Marsh
f8932ec12b Add some TODOs around import tracking 2022-11-11 19:07:40 -05:00
166 changed files with 5820 additions and 1065 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.112
rev: v0.0.119
hooks:
- id: ruff

28
Cargo.lock generated
View File

@@ -427,11 +427,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"time",
"wasm-bindgen",
"winapi 0.3.9",
]
@@ -933,11 +930,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.112-dev.0"
version = "0.0.119-dev.0"
dependencies = [
"anyhow",
"clap 4.0.22",
"configparser",
"fnv",
"once_cell",
"regex",
"ruff",
@@ -2240,7 +2238,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.112"
version = "0.0.119"
dependencies = [
"anyhow",
"assert_cmd",
@@ -2256,6 +2254,7 @@ dependencies = [
"dirs 4.0.0",
"fern",
"filetime",
"fnv",
"getrandom 0.2.8",
"glob",
"insta",
@@ -2287,7 +2286,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.112"
version = "0.0.119"
dependencies = [
"anyhow",
"clap 4.0.22",
@@ -2781,17 +2780,6 @@ dependencies = [
"syn",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi 0.3.9",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@@ -3078,12 +3066,6 @@ version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.112"
version = "0.0.119"
edition = "2021"
[lib]
@@ -16,13 +16,14 @@ name = "ruff"
anyhow = { version = "1.0.66" }
bincode = { version = "1.3.3" }
bitflags = { version = "1.3.2" }
chrono = { version = "0.4.21" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }
clap = { version = "4.0.1", features = ["derive"] }
colored = { version = "2.0.0" }
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" }

116
README.md
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,17 +48,19 @@ 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. [Ruff-specific rules](#ruff-specific-rules)
13. [Meta rules](#meta-rules)
3. [isort](#isort)
4. [pydocstyle](#pydocstyle)
5. [pyupgrade](#pyupgrade)
6. [pep8-naming](#pep8-naming)
7. [flake8-comprehensions](#flake8-comprehensions)
8. [flake8-bugbear](#flake8-bugbear)
9. [flake8-builtins](#flake8-builtins)
10. [flake8-print](#flake8-print)
11. [flake8-quotes](#flake8-quotes)
12. [flake8-annotations](#flake8-annotations)
13. [flake8-2020](#flake8-2020)
14. [Ruff-specific rules](#ruff-specific-rules)
15. [Meta rules](#meta-rules)
5. [Editor Integrations](#editor-integrations)
6. [FAQ](#faq)
7. [Development](#development)
@@ -98,7 +100,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.112
rev: v0.0.116
hooks:
- id: ruff
```
@@ -334,7 +336,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__` | |
@@ -408,7 +410,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") | 🛠 |
@@ -439,9 +441,10 @@ 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 | 🛠 |
| U010 | UnnecessaryFutureImport | Unnessary __future__ import `...` for target Python version | |
| U011 | UnnecessaryLRUCacheParams | Unnessary parameters to functools.lru_cache | 🛠 |
| 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` | 🛠 |
| U012 | UnnecessaryEncodeUTF8 | Unnecessary call to `encode` as UTF-8 | 🛠 |
### pep8-naming
@@ -465,6 +468,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.
@@ -494,24 +510,30 @@ 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 | |
| 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
@@ -561,6 +583,23 @@ For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/2
| ANN206 | MissingReturnTypeClassMethod | Missing return type annotation for classmethod `...` | |
| ANN401 | DynamicallyTypedExpression | Dynamically typed expressions (typing.Any) are disallowed in `...` | |
### flake8-2020
For more, see [flake8-2020](https://pypi.org/project/flake8-2020/1.7.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| YTT101 | SysVersionSlice3Referenced | `sys.version[:3]` referenced (python3.10), use `sys.version_info` | |
| YTT102 | SysVersion2Referenced | `sys.version[2]` referenced (python3.10), use `sys.version_info` | |
| YTT103 | SysVersionCmpStr3 | `sys.version` compared to string (python3.10), use `sys.version_info` | |
| YTT201 | SysVersionInfo0Eq3Referenced | `sys.version_info[0] == 3` referenced (python4), use `>=` | |
| YTT202 | SixPY3Referenced | `six.PY3` referenced (python4), use `not six.PY2` | |
| YTT203 | SysVersionInfo1CmpInt | `sys.version_info[1]` compared to integer (python4), compare `sys.version_info` to tuple | |
| YTT204 | SysVersionInfoMinorCmpInt | `sys.version_info.minor` compared to integer (python4), compare `sys.version_info` to tuple | |
| YTT301 | SysVersion0Referenced | `sys.version[0]` referenced (python10), use `sys.version_info` | |
| YTT302 | SysVersionCmpStr10 | `sys.version` compared to string (python10), use `sys.version_info` | |
| YTT303 | SysVersionSlice1Referenced | `sys.version[:1]` referenced (python10), use `sys.version_info` | |
### Ruff-specific rules
| Code | Name | Message | Fix |
@@ -666,8 +705,10 @@ 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/) (20/32)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (14/34)
- [`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)
Beyond rule-set parity, Ruff suffers from the following limitations vis-à-vis Flake8:
@@ -688,11 +729,13 @@ 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/) (20/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (25/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),
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (14/34).
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (15/34).
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.
@@ -714,9 +757,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.112"
version = "0.0.119"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.112"
version = "0.0.119"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.112-dev.0"
version = "0.0.119-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

108
resources/test/fixtures/B019.py vendored Normal file
View File

@@ -0,0 +1,108 @@
"""
Should emit:
B019 - on lines 73, 77, 81, 85, 89, 93, 97, 101
"""
import functools
from functools import cache, cached_property, lru_cache
def some_other_cache():
...
@functools.cache
def compute_func(self, y):
...
class Foo:
def __init__(self, x):
self.x = x
def compute_method(self, y):
...
@some_other_cache
def user_cached_instance_method(self, y):
...
@classmethod
@functools.cache
def cached_classmethod(cls, y):
...
@classmethod
@cache
def other_cached_classmethod(cls, y):
...
@classmethod
@functools.lru_cache
def lru_cached_classmethod(cls, y):
...
@classmethod
@lru_cache
def other_lru_cached_classmethod(cls, y):
...
@staticmethod
@functools.cache
def cached_staticmethod(y):
...
@staticmethod
@cache
def other_cached_staticmethod(y):
...
@staticmethod
@functools.lru_cache
def lru_cached_staticmethod(y):
...
@staticmethod
@lru_cache
def other_lru_cached_staticmethod(y):
...
@functools.cached_property
def some_cached_property(self):
...
@cached_property
def some_other_cached_property(self):
...
# Remaining methods should emit B019
@functools.cache
def cached_instance_method(self, y):
...
@cache
def another_cached_instance_method(self, y):
...
@functools.cache()
def called_cached_instance_method(self, y):
...
@cache()
def another_called_cached_instance_method(self, y):
...
@functools.lru_cache
def lru_cached_instance_method(self, y):
...
@lru_cache
def another_lru_cached_instance_method(self, y):
...
@functools.lru_cache()
def called_lru_cached_instance_method(self, y):
...
@lru_cache()
def another_called_lru_cached_instance_method(self, y):
...

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]):
...

View File

@@ -28,7 +28,6 @@ from blah import ClassA, ClassB, ClassC
if TYPE_CHECKING:
from models import Fruit, Nut, Vegetable
if TYPE_CHECKING:
import shelve
import importlib

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"]

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

@@ -1,5 +1,14 @@
from __future__ import annotations, nested_scopes, generators
from __future__ import nested_scopes, generators
from __future__ import with_statement, unicode_literals
from __future__ import absolute_import, division
from __future__ import generator_stop
from __future__ import print_function, generator_stop
from __future__ import invalid_module, generators
if True:
from __future__ import generator_stop
from __future__ import generators
if True:
from __future__ import generator_stop
from __future__ import invalid_module, generators

52
resources/test/fixtures/U012.py vendored Normal file
View File

@@ -0,0 +1,52 @@
# ASCII literals should be replaced by a bytes literal
"foo".encode("utf-8") # b"foo"
"foo".encode("u8") # b"foo"
"foo".encode() # b"foo"
"foo".encode("UTF8") # b"foo"
U"foo".encode("utf-8") # b"foo"
"foo".encode(encoding="utf-8") # b"foo"
"""
Lorem
Ipsum
""".encode(
"utf-8"
)
# b"""
# Lorem
#
# Ipsum
# """
# `encode` on variables should not be processed.
string = "hello there"
string.encode("utf-8")
bar = "bar"
f"foo{bar}".encode("utf-8") # f"foo{bar}".encode()
encoding = "latin"
"foo".encode(encoding)
f"foo{bar}".encode(encoding)
# `encode` with custom args and kwargs should not be processed.
"foo".encode("utf-8", errors="replace")
"foo".encode("utf-8", "replace")
"foo".encode(errors="replace")
"foo".encode(encoding="utf-8", errors="replace")
# `encode` with custom args and kwargs on unicode should not be processed.
"unicode text©".encode("utf-8", errors="replace")
"unicode text©".encode("utf-8", "replace")
"unicode text©".encode(errors="replace")
"unicode text©".encode(encoding="utf-8", errors="replace")
# Unicode literals should only be stripped of default encoding.
"unicode text©".encode("utf-8") # "unicode text©".encode()
"unicode text©".encode()
"unicode text©".encode(encoding="UTF8") # "unicode text©".encode()
r"fo\o".encode("utf-8") # br"fo\o"
u"foo".encode("utf-8") # b"foo"
R"fo\o".encode("utf-8") # br"fo\o"
U"foo".encode("utf-8") # b"foo"
print("foo".encode()) # print(b"foo")

12
resources/test/fixtures/YTT101.py vendored Normal file
View File

@@ -0,0 +1,12 @@
import sys
from sys import version, version as v
print(sys.version)
print(sys.version[:3])
print(version[:3])
print(v[:3])
# the tool is timid and only flags certain numeric slices
i = 3
print(sys.version[:i])

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

@@ -0,0 +1,5 @@
import sys
from sys import version
py_minor = sys.version[2]
py_minor = version[2]

8
resources/test/fixtures/YTT103.py vendored Normal file
View File

@@ -0,0 +1,8 @@
import sys
from sys import version
version < "3.5"
sys.version < "3.5"
sys.version <= "3.5"
sys.version > "3.5"
sys.version >= "3.5"

10
resources/test/fixtures/YTT201.py vendored Normal file
View File

@@ -0,0 +1,10 @@
import sys
from sys import version_info
print("{}.{}".format(*sys.version_info))
PY3 = sys.version_info[0] >= 3
PY3 = sys.version_info[0] == 3
PY3 = version_info[0] == 3
PY2 = sys.version_info[0] != 3
PY2 = version_info[0] != 3

7
resources/test/fixtures/YTT202.py vendored Normal file
View File

@@ -0,0 +1,7 @@
import six
from six import PY3
if six.PY3:
print("3")
if PY3:
print("3")

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

@@ -0,0 +1,5 @@
import sys
from sys import version_info
sys.version_info[1] >= 5
version_info[1] < 6

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

@@ -0,0 +1,5 @@
import sys
from sys import version_info
sys.version_info.minor <= 7
version_info.minor > 8

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

@@ -0,0 +1,5 @@
import sys
from sys import version
py_major = sys.version[0]
py_major = version[0]

8
resources/test/fixtures/YTT302.py vendored Normal file
View File

@@ -0,0 +1,8 @@
import sys
from sys import version
version < "3"
sys.version < "3"
sys.version <= "3"
sys.version > "3"
sys.version >= "3"

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

@@ -0,0 +1,5 @@
import sys
from sys import version
print(sys.version[:1])
print(version[:1])

View File

@@ -1 +1,14 @@
from collections import Collection
from line_with_88 import aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
from line_with_89 import (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
)
if indented:
from line_with_88 import aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
from line_with_89 import aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
from line_with_90 import aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
from line_with_91 import aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
from line_with_92 import aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
from line_with_93 import (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
)

View File

@@ -0,0 +1,12 @@
import StringIO
import glob
import os
import shutil
import tempfile
import time
from subprocess import PIPE, Popen, STDOUT
from module import Class, CONSTANT, function, BASIC, Apple
import foo
import FOO
import BAR
import bar

View File

@@ -0,0 +1,2 @@
[tool.ruff]
line-length = 88

View File

@@ -0,0 +1,4 @@
import sys
import leading_prefix
import os
from . import leading_prefix

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.112"
version = "0.0.119"
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

@@ -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,23 +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_annotations, flake8_bugbear, flake8_builtins, flake8_comprehensions,
flake8_print, pep8_naming, pycodestyle, pydocstyle, pyflakes, pyupgrade,
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; 10] = [
"collections",
"collections.abc",
"contextlib",
"functools",
"re",
"typing",
"typing.io",
"typing.re",
"typing_extensions",
"weakref",
];
pub struct Checker<'a> {
// Input data.
@@ -62,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>,
@@ -86,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> {
@@ -105,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(),
@@ -165,15 +159,10 @@ 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_module(&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))
}
}
@@ -203,7 +192,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;
}
@@ -344,6 +339,15 @@ where
if self.settings.enabled.contains(&CheckCode::B018) {
flake8_bugbear::plugins::useless_expression(self, body);
}
if self.settings.enabled.contains(&CheckCode::B019) {
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);
@@ -398,14 +402,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),
));
}
}
}
@@ -449,6 +453,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 {
@@ -513,13 +525,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) =
@@ -578,16 +615,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);
}
}
}
@@ -601,6 +641,12 @@ where
}
}
if let Some("__future__") = module.as_deref() {
if self.settings.enabled.contains(&CheckCode::U010) {
pyupgrade::plugins::unnecessary_future_import(self, stmt, names);
}
}
for alias in names {
if let Some("__future__") = module.as_deref() {
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
@@ -608,6 +654,7 @@ where
name.to_string(),
Binding {
kind: BindingKind::FutureImportation,
// Always mark `__future__` imports as used.
used: Some((
self.scopes[*(self
.scope_stack
@@ -633,14 +680,6 @@ where
}
}
if self.settings.enabled.contains(&CheckCode::U010) {
pyupgrade::plugins::unnecessary_future_import(
self,
stmt,
&alias.node.name,
);
}
if self.settings.enabled.contains(&CheckCode::F404) && !self.futures_allowed
{
self.add_check(Check::new(
@@ -708,7 +747,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),
},
)
@@ -800,6 +858,9 @@ 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) {
@@ -840,6 +901,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) {
@@ -866,6 +934,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,
@@ -885,6 +956,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,
@@ -907,6 +981,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);
}
@@ -981,9 +1058,17 @@ where
pyupgrade::plugins::use_pep604_annotation(self, expr, value, slice);
}
if self.match_typing_module(value, "Literal") {
if self.match_typing_module(&collect_call_paths(value), "Literal") {
self.in_literal = true;
}
if self.settings.enabled.contains(&CheckCode::YTT101)
|| self.settings.enabled.contains(&CheckCode::YTT102)
|| self.settings.enabled.contains(&CheckCode::YTT301)
|| self.settings.enabled.contains(&CheckCode::YTT303)
{
flake8_2020::plugins::subscript(self, value, slice);
}
}
ExprKind::Tuple { elts, ctx } | ExprKind::List { elts, ctx } => {
if matches!(ctx, ExprContext::Store) {
@@ -1001,42 +1086,56 @@ where
}
}
}
ExprKind::Name { id, ctx } => match ctx {
ExprContext::Load => {
// 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"))
{
pyupgrade::plugins::use_pep585_annotation(self, expr, id);
}
self.handle_node_load(expr);
}
ExprContext::Store => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
id,
Range::from_located(expr),
) {
self.add_check(check);
ExprKind::Name { id, ctx } => {
match ctx {
ExprContext::Load => {
// Ex) List[...]
if self.settings.enabled.contains(&CheckCode::U006)
&& self.settings.target_version >= PythonVersion::Py39
&& typing::is_pep585_builtin(
expr,
&self.from_imports,
&self.import_aliases,
)
{
pyupgrade::plugins::use_pep585_annotation(self, expr, id);
}
self.handle_node_load(expr);
}
ExprContext::Store => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
id,
Range::from_located(expr),
) {
self.add_check(check);
}
}
self.check_builtin_shadowing(id, Range::from_located(expr), true);
self.check_builtin_shadowing(id, Range::from_located(expr), true);
self.handle_node_store(expr, self.current_parent());
self.handle_node_store(expr, self.current_parent());
}
ExprContext::Del => self.handle_node_delete(expr),
}
ExprContext::Del => self.handle_node_delete(expr),
},
if self.settings.enabled.contains(&CheckCode::YTT202) {
flake8_2020::plugins::name_or_attribute(self, expr);
}
}
ExprKind::Attribute { attr, .. } => {
// 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);
}
if self.settings.enabled.contains(&CheckCode::YTT202) {
flake8_2020::plugins::name_or_attribute(self, expr);
}
}
ExprKind::Call {
func,
@@ -1052,6 +1151,10 @@ where
pyupgrade::plugins::super_call_with_parameters(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::U012) {
pyupgrade::plugins::unnecessary_encode_utf8(self, expr, func, args, keywords);
}
// flake8-print
if self.settings.enabled.contains(&CheckCode::T201)
|| self.settings.enabled.contains(&CheckCode::T203)
@@ -1078,11 +1181,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) {
@@ -1422,6 +1538,25 @@ where
.into_iter(),
);
}
if self.settings.enabled.contains(&CheckCode::YTT103)
|| self.settings.enabled.contains(&CheckCode::YTT201)
|| self.settings.enabled.contains(&CheckCode::YTT203)
|| self.settings.enabled.contains(&CheckCode::YTT204)
|| self.settings.enabled.contains(&CheckCode::YTT302)
{
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),
@@ -1431,6 +1566,14 @@ where
self.deferred_string_annotations
.push((Range::from_located(expr), value));
}
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, .. } => {
// Visit the arguments, but avoid the body, which will be deferred.
@@ -1502,12 +1645,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_module(&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_module(&call_path, "cast") {
self.visit_expr(func);
if !args.is_empty() {
self.visit_annotation(&args[0]);
@@ -1515,12 +1659,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_module(&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_module(&call_path, "TypeVar") {
self.visit_expr(func);
for expr in args.iter().skip(1) {
self.visit_annotation(expr);
@@ -1537,7 +1681,7 @@ where
}
}
}
} else if self.match_typing_module(func, "NamedTuple") {
} else if self.match_typing_module(&call_path, "NamedTuple") {
self.visit_expr(func);
// Ex) NamedTuple("a", [("a", int)])
@@ -1569,7 +1713,7 @@ where
let KeywordData { value, .. } = &keyword.node;
self.visit_annotation(value);
}
} else if self.match_typing_module(func, "TypedDict") {
} else if self.match_typing_module(&call_path, "TypedDict") {
self.visit_expr(func);
// Ex) TypedDict("a", {"a": int})
@@ -1608,7 +1752,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]
@@ -2020,7 +2168,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;
}
}
@@ -2047,32 +2198,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,
)
}
}
@@ -2151,7 +2292,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;
}
@@ -2171,6 +2318,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 {
@@ -2337,7 +2487,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))
@@ -2346,25 +2496,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);
}
_ => {}
}
@@ -2381,16 +2531,20 @@ impl<'a> Checker<'a> {
.iter()
.map(|index| self.parents[*index])
.collect();
let removal_fn = match kind {
match match kind {
ImportKind::Import => pyflakes::fixes::remove_unused_imports,
ImportKind::ImportFrom => pyflakes::fixes::remove_unused_import_froms,
};
match removal_fn(self.locator, &full_names, child, parent, &deleted) {
Ok(fix) => Some(fix),
}(
self.locator, &full_names, child, parent, &deleted
) {
Ok(fix) => {
if fix.patch.content.is_empty() || fix.patch.content == "pass" {
self.deletions.insert(defined_by);
}
Some(fix)
}
Err(e) => {
error!("Failed to fix unused imports: {}", e);
error!("Failed to remove unused imports: {}", e);
None
}
}
@@ -2401,7 +2555,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),
@@ -2409,7 +2563,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),
@@ -2602,8 +2756,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,14 +87,20 @@ pub enum CheckCode {
B009,
B010,
B011,
B012,
B013,
B014,
B015,
B016,
B017,
B018,
B019,
B021,
B022,
B024,
B025,
B026,
B027,
// flake8-comprehensions
C400,
C401,
@@ -132,6 +138,17 @@ pub enum CheckCode {
ANN205,
ANN206,
ANN401,
// flake8-2020
YTT101,
YTT102,
YTT103,
YTT201,
YTT202,
YTT203,
YTT204,
YTT301,
YTT302,
YTT303,
// pyupgrade
U001,
U002,
@@ -144,6 +161,7 @@ pub enum CheckCode {
U009,
U010,
U011,
U012,
// pydocstyle
D100,
D101,
@@ -207,6 +225,13 @@ pub enum CheckCode {
N818,
// isort
I001,
// flake8-bandit
S101,
S102,
S104,
S105,
S106,
S107,
// Ruff
RUF001,
RUF002,
@@ -223,12 +248,14 @@ pub enum CheckCategory {
Pydocstyle,
Pyupgrade,
PEP8Naming,
Flake8Bandit,
Flake8Comprehensions,
Flake8Bugbear,
Flake8Builtins,
Flake8Print,
Flake8Quotes,
Flake8Annotations,
Flake82020,
Ruff,
Meta,
}
@@ -239,12 +266,14 @@ 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",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
CheckCategory::Flake8Annotations => "flake8-annotations",
CheckCategory::Flake82020 => "flake8-2020",
CheckCategory::Pyupgrade => "pyupgrade",
CheckCategory::Pydocstyle => "pydocstyle",
CheckCategory::PEP8Naming => "pep8-naming",
@@ -272,9 +301,11 @@ impl CheckCategory {
CheckCategory::Flake8Annotations => {
Some("https://pypi.org/project/flake8-annotations/2.9.1/")
}
CheckCategory::Flake82020 => Some("https://pypi.org/project/flake8-2020/1.7.0/"),
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,
}
@@ -356,18 +387,24 @@ pub enum CheckKind {
StripWithMultiCharacters,
MutableArgumentDefault,
UnusedLoopControlVariable(String),
FunctionCallArgumentDefault,
FunctionCallArgumentDefault(Option<String>),
GetAttrWithConstant,
SetAttrWithConstant,
DoNotAssertFalse,
JumpStatementInFinally(String),
RedundantTupleInExceptionHandler(String),
DuplicateHandlerException(Vec<String>),
UselessComparison,
CannotRaiseLiteral,
NoAssertRaisesException,
UselessExpression,
CachedInstanceMethod,
FStringDocstring,
UselessContextlibSuppress,
AbstractBaseClassWithoutAbstractMethod(String),
DuplicateTryBlockException(String),
StarArgUnpackingAfterKeywordArg,
EmptyMethodWithoutAbstractDecorator(String),
// flake8-comprehensions
UnnecessaryGeneratorList,
UnnecessaryGeneratorSet,
@@ -405,6 +442,17 @@ pub enum CheckKind {
MissingReturnTypeStaticMethod(String),
MissingReturnTypeClassMethod(String),
DynamicallyTypedExpression(String),
// flake8-2020
SysVersionSlice3Referenced,
SysVersion2Referenced,
SysVersionCmpStr3,
SysVersionInfo0Eq3Referenced,
SixPY3Referenced,
SysVersionInfo1CmpInt,
SysVersionInfoMinorCmpInt,
SysVersion0Referenced,
SysVersionCmpStr10,
SysVersionSlice1Referenced,
// pyupgrade
TypeOfPrimitive(Primitive),
UnnecessaryAbspath,
@@ -415,8 +463,9 @@ pub enum CheckKind {
UsePEP604Annotation,
SuperCallWithParameters,
PEP3120UnnecessaryCodingComment,
UnnecessaryFutureImport(String),
UnnecessaryFutureImport(Vec<String>),
UnnecessaryLRUCacheParams,
UnnecessaryEncodeUTF8,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -480,6 +529,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),
@@ -573,10 +629,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())
}
@@ -585,8 +644,13 @@ impl CheckCode {
CheckCode::B016 => CheckKind::CannotRaiseLiteral,
CheckCode::B017 => CheckKind::NoAssertRaisesException,
CheckCode::B018 => CheckKind::UselessExpression,
CheckCode::B019 => CheckKind::CachedInstanceMethod,
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,
@@ -637,6 +701,17 @@ impl CheckCode {
CheckCode::ANN205 => CheckKind::MissingReturnTypeStaticMethod("...".to_string()),
CheckCode::ANN206 => CheckKind::MissingReturnTypeClassMethod("...".to_string()),
CheckCode::ANN401 => CheckKind::DynamicallyTypedExpression("...".to_string()),
// flake8-2020
CheckCode::YTT101 => CheckKind::SysVersionSlice3Referenced,
CheckCode::YTT102 => CheckKind::SysVersion2Referenced,
CheckCode::YTT103 => CheckKind::SysVersionCmpStr3,
CheckCode::YTT201 => CheckKind::SysVersionInfo0Eq3Referenced,
CheckCode::YTT202 => CheckKind::SixPY3Referenced,
CheckCode::YTT203 => CheckKind::SysVersionInfo1CmpInt,
CheckCode::YTT204 => CheckKind::SysVersionInfoMinorCmpInt,
CheckCode::YTT301 => CheckKind::SysVersion0Referenced,
CheckCode::YTT302 => CheckKind::SysVersionCmpStr10,
CheckCode::YTT303 => CheckKind::SysVersionSlice1Referenced,
// pyupgrade
CheckCode::U001 => CheckKind::UselessMetaclassType,
CheckCode::U002 => CheckKind::UnnecessaryAbspath,
@@ -650,8 +725,9 @@ impl CheckCode {
CheckCode::U007 => CheckKind::UsePEP604Annotation,
CheckCode::U008 => CheckKind::SuperCallWithParameters,
CheckCode::U009 => CheckKind::PEP3120UnnecessaryCodingComment,
CheckCode::U010 => CheckKind::UnnecessaryFutureImport("...".to_string()),
CheckCode::U010 => CheckKind::UnnecessaryFutureImport(vec!["...".to_string()]),
CheckCode::U011 => CheckKind::UnnecessaryLRUCacheParams,
CheckCode::U012 => CheckKind::UnnecessaryEncodeUTF8,
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -731,6 +807,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'),
@@ -799,14 +882,20 @@ 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,
CheckCode::B016 => CheckCategory::Flake8Bugbear,
CheckCode::B017 => CheckCategory::Flake8Bugbear,
CheckCode::B018 => CheckCategory::Flake8Bugbear,
CheckCode::B019 => 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,
@@ -840,6 +929,16 @@ impl CheckCode {
CheckCode::ANN205 => CheckCategory::Flake8Annotations,
CheckCode::ANN206 => CheckCategory::Flake8Annotations,
CheckCode::ANN401 => CheckCategory::Flake8Annotations,
CheckCode::YTT101 => CheckCategory::Flake82020,
CheckCode::YTT102 => CheckCategory::Flake82020,
CheckCode::YTT103 => CheckCategory::Flake82020,
CheckCode::YTT201 => CheckCategory::Flake82020,
CheckCode::YTT202 => CheckCategory::Flake82020,
CheckCode::YTT203 => CheckCategory::Flake82020,
CheckCode::YTT204 => CheckCategory::Flake82020,
CheckCode::YTT301 => CheckCategory::Flake82020,
CheckCode::YTT302 => CheckCategory::Flake82020,
CheckCode::YTT303 => CheckCategory::Flake82020,
CheckCode::U001 => CheckCategory::Pyupgrade,
CheckCode::U002 => CheckCategory::Pyupgrade,
CheckCode::U003 => CheckCategory::Pyupgrade,
@@ -851,6 +950,7 @@ impl CheckCode {
CheckCode::U009 => CheckCategory::Pyupgrade,
CheckCode::U010 => CheckCategory::Pyupgrade,
CheckCode::U011 => CheckCategory::Pyupgrade,
CheckCode::U012 => CheckCategory::Pyupgrade,
CheckCode::D100 => CheckCategory::Pydocstyle,
CheckCode::D101 => CheckCategory::Pydocstyle,
CheckCode::D102 => CheckCategory::Pydocstyle,
@@ -911,6 +1011,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,
@@ -980,18 +1086,24 @@ 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,
CheckKind::CannotRaiseLiteral => &CheckCode::B016,
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::UselessExpression => &CheckCode::B018,
CheckKind::CachedInstanceMethod => &CheckCode::B019,
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,
@@ -1029,6 +1141,17 @@ impl CheckKind {
CheckKind::MissingReturnTypeStaticMethod(_) => &CheckCode::ANN205,
CheckKind::MissingReturnTypeClassMethod(_) => &CheckCode::ANN206,
CheckKind::DynamicallyTypedExpression(_) => &CheckCode::ANN401,
// flake8-2020
CheckKind::SysVersionSlice3Referenced => &CheckCode::YTT101,
CheckKind::SysVersion2Referenced => &CheckCode::YTT102,
CheckKind::SysVersionCmpStr3 => &CheckCode::YTT103,
CheckKind::SysVersionInfo0Eq3Referenced => &CheckCode::YTT201,
CheckKind::SixPY3Referenced => &CheckCode::YTT202,
CheckKind::SysVersionInfo1CmpInt => &CheckCode::YTT203,
CheckKind::SysVersionInfoMinorCmpInt => &CheckCode::YTT204,
CheckKind::SysVersion0Referenced => &CheckCode::YTT301,
CheckKind::SysVersionCmpStr10 => &CheckCode::YTT302,
CheckKind::SysVersionSlice1Referenced => &CheckCode::YTT303,
// pyupgrade
CheckKind::TypeOfPrimitive(_) => &CheckCode::U003,
CheckKind::UnnecessaryAbspath => &CheckCode::U002,
@@ -1041,6 +1164,7 @@ impl CheckKind {
CheckKind::PEP3120UnnecessaryCodingComment => &CheckCode::U009,
CheckKind::UnnecessaryFutureImport(_) => &CheckCode::U010,
CheckKind::UnnecessaryLRUCacheParams => &CheckCode::U011,
CheckKind::UnnecessaryEncodeUTF8 => &CheckCode::U012,
// pydocstyle
CheckKind::BlankLineAfterLastSection(_) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(_) => &CheckCode::D410,
@@ -1104,6 +1228,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,
@@ -1132,7 +1263,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()
@@ -1269,40 +1400,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 \
@@ -1322,7 +1460,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`."
@@ -1331,16 +1469,35 @@ impl CheckKind {
CheckKind::UselessExpression => {
"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"
.to_string(),
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()
@@ -1473,6 +1630,38 @@ impl CheckKind {
CheckKind::DynamicallyTypedExpression(name) => {
format!("Dynamically typed expressions (typing.Any) are disallowed in `{name}`")
}
// flake8-2020
CheckKind::SysVersionSlice3Referenced => {
"`sys.version[:3]` referenced (python3.10), use `sys.version_info`".to_string()
}
CheckKind::SysVersion2Referenced => {
"`sys.version[2]` referenced (python3.10), use `sys.version_info`".to_string()
}
CheckKind::SysVersionCmpStr3 => {
"`sys.version` compared to string (python3.10), use `sys.version_info`".to_string()
}
CheckKind::SysVersionInfo0Eq3Referenced => {
"`sys.version_info[0] == 3` referenced (python4), use `>=`".to_string()
}
CheckKind::SixPY3Referenced => {
"`six.PY3` referenced (python4), use `not six.PY2`".to_string()
}
CheckKind::SysVersionInfo1CmpInt => "`sys.version_info[1]` compared to integer \
(python4), compare `sys.version_info` to tuple"
.to_string(),
CheckKind::SysVersionInfoMinorCmpInt => "`sys.version_info.minor` compared to integer \
(python4), compare `sys.version_info` to \
tuple"
.to_string(),
CheckKind::SysVersion0Referenced => {
"`sys.version[0]` referenced (python10), use `sys.version_info`".to_string()
}
CheckKind::SysVersionCmpStr10 => {
"`sys.version` compared to string (python10), use `sys.version_info`".to_string()
}
CheckKind::SysVersionSlice1Referenced => {
"`sys.version[:1]` referenced (python10), use `sys.version_info`".to_string()
}
// pyupgrade
CheckKind::TypeOfPrimitive(primitive) => {
format!("Use `{}` instead of `type(...)`", primitive.builtin())
@@ -1498,12 +1687,19 @@ impl CheckKind {
CheckKind::SuperCallWithParameters => {
"Use `super()` instead of `super(__class__, self)`".to_string()
}
CheckKind::UnnecessaryFutureImport(name) => {
format!("Unnessary __future__ import `{name}` for target Python version")
CheckKind::UnnecessaryFutureImport(names) => {
if names.len() == 1 {
let import = &names[0];
format!("Unnecessary `__future__` import `{import}` for target Python version")
} else {
let imports = names.iter().map(|name| format!("`{name}`")).join(", ");
format!("Unnecessary `__future__` imports {imports} for target Python version")
}
}
CheckKind::UnnecessaryLRUCacheParams => {
"Unnessary 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
CheckKind::FitsOnOneLine => "One-line docstring should fit on one line".to_string(),
CheckKind::BlankLineAfterSummary => {
@@ -1557,7 +1753,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()
@@ -1665,10 +1861,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!(
@@ -1713,16 +1924,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(),
}
@@ -1770,6 +1981,8 @@ impl CheckKind {
| CheckKind::UnnecessaryAbspath
| CheckKind::UnnecessaryCollectionCall(_)
| CheckKind::UnnecessaryComprehension(_)
| CheckKind::UnnecessaryEncodeUTF8
| CheckKind::UnnecessaryFutureImport(_)
| CheckKind::UnnecessaryGeneratorDict
| CheckKind::UnnecessaryGeneratorList
| CheckKind::UnnecessaryGeneratorSet

View File

@@ -47,15 +47,21 @@ pub enum CheckCodePrefix {
B01,
B010,
B011,
B012,
B013,
B014,
B015,
B016,
B017,
B018,
B019,
B02,
B021,
B022,
B024,
B025,
B026,
B027,
C,
C4,
C40,
@@ -244,6 +250,15 @@ pub enum CheckCodePrefix {
RUF001,
RUF002,
RUF003,
S,
S1,
S10,
S101,
S102,
S104,
S105,
S106,
S107,
T,
T2,
T20,
@@ -264,6 +279,7 @@ pub enum CheckCodePrefix {
U01,
U010,
U011,
U012,
W,
W2,
W29,
@@ -271,6 +287,23 @@ pub enum CheckCodePrefix {
W6,
W60,
W605,
YTT,
YTT1,
YTT10,
YTT101,
YTT102,
YTT103,
YTT2,
YTT20,
YTT201,
YTT202,
YTT203,
YTT204,
YTT3,
YTT30,
YTT301,
YTT302,
YTT303,
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
@@ -345,14 +378,20 @@ impl CheckCodePrefix {
CheckCode::B009,
CheckCode::B010,
CheckCode::B011,
CheckCode::B012,
CheckCode::B013,
CheckCode::B014,
CheckCode::B015,
CheckCode::B016,
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
],
CheckCodePrefix::B0 => vec![
CheckCode::B002,
@@ -365,14 +404,20 @@ impl CheckCodePrefix {
CheckCode::B009,
CheckCode::B010,
CheckCode::B011,
CheckCode::B012,
CheckCode::B013,
CheckCode::B014,
CheckCode::B015,
CheckCode::B016,
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
],
CheckCodePrefix::B00 => vec![
CheckCode::B002,
@@ -395,24 +440,39 @@ impl CheckCodePrefix {
CheckCodePrefix::B01 => vec![
CheckCode::B010,
CheckCode::B011,
CheckCode::B012,
CheckCode::B013,
CheckCode::B014,
CheckCode::B015,
CheckCode::B016,
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
],
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],
CheckCodePrefix::B016 => vec![CheckCode::B016],
CheckCodePrefix::B017 => vec![CheckCode::B017],
CheckCodePrefix::B018 => vec![CheckCode::B018],
CheckCodePrefix::B02 => vec![CheckCode::B025, CheckCode::B026],
CheckCodePrefix::B019 => vec![CheckCode::B019],
CheckCodePrefix::B02 => vec![
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
],
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,
@@ -965,6 +1025,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],
@@ -982,6 +1072,7 @@ impl CheckCodePrefix {
CheckCode::U009,
CheckCode::U010,
CheckCode::U011,
CheckCode::U012,
],
CheckCodePrefix::U0 => vec![
CheckCode::U001,
@@ -995,6 +1086,7 @@ impl CheckCodePrefix {
CheckCode::U009,
CheckCode::U010,
CheckCode::U011,
CheckCode::U012,
],
CheckCodePrefix::U00 => vec![
CheckCode::U001,
@@ -1016,9 +1108,10 @@ impl CheckCodePrefix {
CheckCodePrefix::U007 => vec![CheckCode::U007],
CheckCodePrefix::U008 => vec![CheckCode::U008],
CheckCodePrefix::U009 => vec![CheckCode::U009],
CheckCodePrefix::U01 => vec![CheckCode::U010, CheckCode::U011],
CheckCodePrefix::U01 => vec![CheckCode::U010, CheckCode::U011, CheckCode::U012],
CheckCodePrefix::U010 => vec![CheckCode::U010],
CheckCodePrefix::U011 => vec![CheckCode::U011],
CheckCodePrefix::U012 => vec![CheckCode::U012],
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
CheckCodePrefix::W2 => vec![CheckCode::W292],
CheckCodePrefix::W29 => vec![CheckCode::W292],
@@ -1026,6 +1119,44 @@ impl CheckCodePrefix {
CheckCodePrefix::W6 => vec![CheckCode::W605],
CheckCodePrefix::W60 => vec![CheckCode::W605],
CheckCodePrefix::W605 => vec![CheckCode::W605],
CheckCodePrefix::YTT => vec![
CheckCode::YTT101,
CheckCode::YTT102,
CheckCode::YTT103,
CheckCode::YTT201,
CheckCode::YTT202,
CheckCode::YTT203,
CheckCode::YTT204,
CheckCode::YTT301,
CheckCode::YTT302,
CheckCode::YTT303,
],
CheckCodePrefix::YTT1 => vec![CheckCode::YTT101, CheckCode::YTT102, CheckCode::YTT103],
CheckCodePrefix::YTT10 => vec![CheckCode::YTT101, CheckCode::YTT102, CheckCode::YTT103],
CheckCodePrefix::YTT101 => vec![CheckCode::YTT101],
CheckCodePrefix::YTT102 => vec![CheckCode::YTT102],
CheckCodePrefix::YTT103 => vec![CheckCode::YTT103],
CheckCodePrefix::YTT2 => vec![
CheckCode::YTT201,
CheckCode::YTT202,
CheckCode::YTT203,
CheckCode::YTT204,
],
CheckCodePrefix::YTT20 => vec![
CheckCode::YTT201,
CheckCode::YTT202,
CheckCode::YTT203,
CheckCode::YTT204,
],
CheckCodePrefix::YTT201 => vec![CheckCode::YTT201],
CheckCodePrefix::YTT202 => vec![CheckCode::YTT202],
CheckCodePrefix::YTT203 => vec![CheckCode::YTT203],
CheckCodePrefix::YTT204 => vec![CheckCode::YTT204],
CheckCodePrefix::YTT3 => vec![CheckCode::YTT301, CheckCode::YTT302, CheckCode::YTT303],
CheckCodePrefix::YTT30 => vec![CheckCode::YTT301, CheckCode::YTT302, CheckCode::YTT303],
CheckCodePrefix::YTT301 => vec![CheckCode::YTT301],
CheckCodePrefix::YTT302 => vec![CheckCode::YTT302],
CheckCodePrefix::YTT303 => vec![CheckCode::YTT303],
}
}
}
@@ -1073,15 +1204,21 @@ 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,
CheckCodePrefix::B016 => PrefixSpecificity::Explicit,
CheckCodePrefix::B017 => PrefixSpecificity::Explicit,
CheckCodePrefix::B018 => PrefixSpecificity::Explicit,
CheckCodePrefix::B019 => PrefixSpecificity::Explicit,
CheckCodePrefix::B02 => PrefixSpecificity::Tens,
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,
@@ -1270,6 +1407,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,
@@ -1290,6 +1436,7 @@ impl CheckCodePrefix {
CheckCodePrefix::U01 => PrefixSpecificity::Tens,
CheckCodePrefix::U010 => PrefixSpecificity::Explicit,
CheckCodePrefix::U011 => PrefixSpecificity::Explicit,
CheckCodePrefix::U012 => PrefixSpecificity::Explicit,
CheckCodePrefix::W => PrefixSpecificity::Category,
CheckCodePrefix::W2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::W29 => PrefixSpecificity::Tens,
@@ -1297,6 +1444,23 @@ impl CheckCodePrefix {
CheckCodePrefix::W6 => PrefixSpecificity::Hundreds,
CheckCodePrefix::W60 => PrefixSpecificity::Tens,
CheckCodePrefix::W605 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT => PrefixSpecificity::Category,
CheckCodePrefix::YTT1 => PrefixSpecificity::Hundreds,
CheckCodePrefix::YTT10 => PrefixSpecificity::Tens,
CheckCodePrefix::YTT101 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT102 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT103 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::YTT20 => PrefixSpecificity::Tens,
CheckCodePrefix::YTT201 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT202 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT203 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT204 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT3 => PrefixSpecificity::Hundreds,
CheckCodePrefix::YTT30 => PrefixSpecificity::Tens,
CheckCodePrefix::YTT301 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT302 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT303 => PrefixSpecificity::Explicit,
}
}
}

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;
@@ -188,7 +188,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,

1
src/flake8_2020/mod.rs Normal file
View File

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

202
src/flake8_2020/plugins.rs Normal file
View File

@@ -0,0 +1,202 @@
use num_bigint::BigInt;
use rustpython_ast::{Cmpop, Constant, Expr, ExprKind, Located};
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_module_member(
expr,
"sys",
target,
&checker.from_imports,
&checker.import_aliases,
)
}
/// YTT101, YTT102, YTT301, YTT303
pub fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
if is_sys(checker, value, "version") {
match &slice.node {
ExprKind::Slice {
lower: None,
upper: Some(upper),
step: None,
..
} => {
if let ExprKind::Constant {
value: Constant::Int(i),
..
} = &upper.node
{
if *i == BigInt::from(1)
&& checker.settings.enabled.contains(&CheckCode::YTT303)
{
checker.add_check(Check::new(
CheckKind::SysVersionSlice1Referenced,
Range::from_located(value),
));
} else if *i == BigInt::from(3)
&& checker.settings.enabled.contains(&CheckCode::YTT101)
{
checker.add_check(Check::new(
CheckKind::SysVersionSlice3Referenced,
Range::from_located(value),
));
}
}
}
ExprKind::Constant {
value: Constant::Int(i),
..
} => {
if *i == BigInt::from(2) && checker.settings.enabled.contains(&CheckCode::YTT102) {
checker.add_check(Check::new(
CheckKind::SysVersion2Referenced,
Range::from_located(value),
));
} else if *i == BigInt::from(0)
&& checker.settings.enabled.contains(&CheckCode::YTT301)
{
checker.add_check(Check::new(
CheckKind::SysVersion0Referenced,
Range::from_located(value),
));
}
}
_ => {}
}
}
}
/// YTT103, YTT201, YTT203, YTT204, YTT302
pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &[Expr]) {
match &left.node {
ExprKind::Subscript { value, slice, .. } if is_sys(checker, value, "version_info") => {
if let ExprKind::Constant {
value: Constant::Int(i),
..
} = &slice.node
{
if *i == BigInt::from(0) {
if let (
[Cmpop::Eq | Cmpop::NotEq],
[Located {
node:
ExprKind::Constant {
value: Constant::Int(n),
..
},
..
}],
) = (ops, comparators)
{
if *n == BigInt::from(3)
&& checker.settings.enabled.contains(&CheckCode::YTT201)
{
checker.add_check(Check::new(
CheckKind::SysVersionInfo0Eq3Referenced,
Range::from_located(left),
));
}
}
} else if *i == BigInt::from(1) {
if let (
[Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE],
[Located {
node:
ExprKind::Constant {
value: Constant::Int(_),
..
},
..
}],
) = (ops, comparators)
{
if checker.settings.enabled.contains(&CheckCode::YTT203) {
checker.add_check(Check::new(
CheckKind::SysVersionInfo1CmpInt,
Range::from_located(left),
));
}
}
}
}
}
ExprKind::Attribute { value, attr, .. }
if is_sys(checker, value, "version_info") && attr == "minor" =>
{
if let (
[Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE],
[Located {
node:
ExprKind::Constant {
value: Constant::Int(_),
..
},
..
}],
) = (ops, comparators)
{
if checker.settings.enabled.contains(&CheckCode::YTT204) {
checker.add_check(Check::new(
CheckKind::SysVersionInfoMinorCmpInt,
Range::from_located(left),
));
}
}
}
_ => {}
}
if is_sys(checker, left, "version") {
if let (
[Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE],
[Located {
node:
ExprKind::Constant {
value: Constant::Str(s),
..
},
..
}],
) = (ops, comparators)
{
if s.len() == 1 {
if checker.settings.enabled.contains(&CheckCode::YTT302) {
checker.add_check(Check::new(
CheckKind::SysVersionCmpStr10,
Range::from_located(left),
));
}
} else if checker.settings.enabled.contains(&CheckCode::YTT103) {
checker.add_check(Check::new(
CheckKind::SysVersionCmpStr3,
Range::from_located(left),
));
}
}
}
}
/// YTT202
pub fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
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

@@ -1,5 +1,6 @@
use rustpython_ast::{Arguments, Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::helpers::collect_call_paths;
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
@@ -49,10 +50,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_module(&collect_call_paths(annotation), "Any") {
checker.add_check(Check::new(
CheckKind::DynamicallyTypedExpression(name.to_string()),
CheckKind::DynamicallyTypedExpression(func()),
Range::from_located(annotation),
));
};
@@ -100,7 +104,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 +126,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 +149,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 +169,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 +219,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 +242,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 +266,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 +309,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

@@ -0,0 +1,41 @@
use rustpython_ast::{Expr, ExprKind};
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 {
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
pub fn cached_instance_method(checker: &mut Checker, decorator_list: &[Expr]) {
if matches!(checker.current_scope().kind, ScopeKind::Class(_)) {
for decorator in decorator_list {
// TODO(charlie): This should take into account `classmethod-decorators` and
// `staticmethod-decorators`.
if let ExprKind::Name { id, .. } = &decorator.node {
if id == "classmethod" || id == "staticmethod" {
return;
}
}
}
for decorator in decorator_list {
if is_cache_func(
checker,
match &decorator.node {
ExprKind::Call { func, .. } => func,
_ => decorator,
},
) {
checker.add_check(Check::new(
CheckKind::CachedInstanceMethod,
Range::from_located(decorator),
));
}
}
}
}

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,41 +11,54 @@ use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::flake8_bugbear::plugins::mutable_argument_default::is_mutable_func;
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),
))
}
@@ -82,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

@@ -1,10 +1,14 @@
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 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;
@@ -14,15 +18,20 @@ 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 mutable_argument_default;
mod redundant_tuple_in_exception_handler;
mod setattr_with_constant;
@@ -32,4 +41,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,32 +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};
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
@@ -49,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

@@ -7,6 +7,7 @@ use crate::check_ast::Checker;
use crate::checks::CheckCode;
use crate::flake8_print::checks;
/// T201, T203
pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
if let Some(mut check) = checks::print_call(
expr,
@@ -26,7 +27,6 @@ pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
.iter()
.map(|index| checker.parents[*index])
.collect();
match helpers::remove_stmt(
checker.parents[context.defined_by],
context.defined_in.map(|index| checker.parents[index]),
@@ -38,7 +38,7 @@ pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
}
check.amend(fix)
}
Err(e) => error!("Failed to fix unused imports: {}", e),
Err(e) => error!("Failed to remove print call: {}", e),
}
}
}

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)]
@@ -12,45 +10,36 @@ pub enum ImportType {
StandardLibrary,
ThirdParty,
FirstParty,
LocalFolder,
}
pub fn categorize(
module_base: &str,
level: &Option<usize>,
src: &[PathBuf],
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
extra_standard_library: &BTreeSet<String>,
) -> ImportType {
if known_first_party.contains(module_base) {
if level.map(|level| level > 0).unwrap_or(false) {
ImportType::LocalFolder
} else if known_first_party.contains(module_base) {
ImportType::FirstParty
} else if known_third_party.contains(module_base) {
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) {
ImportType::FirstParty
} else {
if find_local(src, module_base) {
ImportType::FirstParty
} else {
ImportType::ThirdParty
}
ImportType::ThirdParty
}
}
static STATIC_CLASSIFICATIONS: Lazy<BTreeMap<&'static str, ImportType>> = Lazy::new(|| {
BTreeMap::from([
("__future__", ImportType::Future),
("__main__", ImportType::FirstParty),
// Force `disutils` to be considered third-party.
("disutils", ImportType::ThirdParty),
// 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,15 +1,19 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use fnv::FnvHashSet;
use itertools::Itertools;
use ropey::RopeBuilder;
use rustpython_ast::{Stmt, StmtKind};
use crate::isort::categorize::{categorize, ImportType};
use crate::isort::types::{AliasData, ImportBlock, ImportFromData, Importable};
use crate::isort::sorting::{member_key, module_key};
use crate::isort::types::{AliasData, ImportBlock, ImportFromData, Importable, OrderedImportBlock};
mod categorize;
pub mod plugins;
pub mod settings;
mod sorting;
pub mod track;
mod types;
@@ -33,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"),
@@ -62,6 +76,7 @@ fn categorize_imports<'a>(
for alias in block.import {
let import_type = categorize(
&alias.module_base(),
&None,
src,
known_first_party,
known_third_party,
@@ -73,10 +88,11 @@ 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(),
import_from.level,
src,
known_first_party,
known_third_party,
@@ -88,10 +104,78 @@ 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
}
pub fn sort_imports(
fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
let mut ordered: OrderedImportBlock = Default::default();
// Sort `StmtKind::Import`.
ordered.import.extend(
block
.import
.into_iter()
.sorted_by_cached_key(|alias| module_key(alias.name, alias.asname)),
);
// Sort `StmtKind::ImportFrom`.
ordered.import_from.extend(
// Include all non-re-exports.
block
.import_from
.into_iter()
.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>>(),
)
})
.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
}
pub fn format_imports(
block: Vec<&Stmt>,
line_length: &usize,
src: &[PathBuf],
@@ -114,45 +198,41 @@ pub fn sort_imports(
// Generate replacement source code.
let mut output = RopeBuilder::new();
let mut first_block = true;
for import_type in [
ImportType::Future,
ImportType::StandardLibrary,
ImportType::ThirdParty,
ImportType::FirstParty,
] {
if let Some(import_block) = block_by_type.get(&import_type) {
// Add a blank line between every section.
if !first_block {
output.append("\n");
for import_block in block_by_type.into_values() {
let import_block = sort_imports(import_block);
// Add a blank line between every section.
if !first_block {
output.append("\n");
} else {
first_block = false;
}
// Format `StmtKind::Import` statements.
for AliasData { name, asname } in import_block.import.iter() {
if let Some(asname) = asname {
output.append(&format!("import {} as {}\n", name, asname));
} else {
first_block = false;
output.append(&format!("import {}\n", name));
}
}
// Format `StmtKind::Import` statements.
for AliasData { name, asname } in import_block.import.iter() {
if let Some(asname) = asname {
output.append(&format!("import {} as {}\n", name, asname));
} else {
output.append(&format!("import {}\n", name));
}
}
// Format `StmtKind::ImportFrom` statements.
for (import_from, aliases) in import_block.import_from.iter() {
let prelude: String = format!("from {} import ", import_from.module_name());
let members: Vec<String> = aliases
.iter()
.map(|AliasData { name, asname }| {
if let Some(asname) = asname {
format!("{} as {}", name, asname)
} else {
name.to_string()
}
})
.collect();
// Format `StmtKind::ImportFrom` statements.
for (import_from, aliases) in import_block.import_from.iter() {
let prelude: String = format!("from {} import ", import_from.module_name());
let members: Vec<String> = aliases
.iter()
.map(|AliasData { name, asname }| {
if let Some(asname) = asname {
format!("{} as {}", name, asname)
} else {
name.to_string()
}
})
.collect();
// Can we fit the import on a single line?
let expected_len: usize =
// Can we fit the import on a single line?
let expected_len: usize =
// `from base import `
prelude.len()
// `member( as alias)?`
@@ -160,36 +240,35 @@ pub fn sort_imports(
// `, `
+ 2 * (members.len() - 1);
if expected_len <= *line_length {
// `from base import `
output.append(&prelude);
// `member( as alias)?(, )?`
for (index, part) in members.into_iter().enumerate() {
if index > 0 {
output.append(", ");
}
output.append(&part);
if expected_len <= *line_length {
// `from base import `
output.append(&prelude);
// `member( as alias)?(, )?`
for (index, part) in members.into_iter().enumerate() {
if index > 0 {
output.append(", ");
}
// `\n`
output.append("\n");
} else {
// `from base import (\n`
output.append(&prelude);
output.append("(");
output.append("\n");
output.append(&part);
}
// `\n`
output.append("\n");
} else {
// `from base import (\n`
output.append(&prelude);
output.append("(");
output.append("\n");
// ` member( as alias)?,\n`
for part in members {
output.append(INDENT);
output.append(&part);
output.append(",");
output.append("\n");
}
// `)\n`
output.append(")");
// ` member( as alias)?,\n`
for part in members {
output.append(INDENT);
output.append(&part);
output.append(",");
output.append("\n");
}
// `)\n`
output.append(")");
output.append("\n");
}
}
}
@@ -214,12 +293,15 @@ mod tests {
#[test_case(Path::new("import_from_after_import.py"))]
#[test_case(Path::new("leading_prefix.py"))]
#[test_case(Path::new("no_reorder_within_section.py"))]
#[test_case(Path::new("order_by_type.py"))]
#[test_case(Path::new("preserve_indentation.py"))]
#[test_case(Path::new("reorder_within_section.py"))]
#[test_case(Path::new("separate_first_party_imports.py"))]
#[test_case(Path::new("separate_future_imports.py"))]
#[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

@@ -5,7 +5,7 @@ use crate::ast::types::Range;
use crate::autofix::{fixer, Fix};
use crate::checks::CheckKind;
use crate::docstrings::helpers::leading_space;
use crate::isort::sort_imports;
use crate::isort::format_imports;
use crate::{Check, Settings, SourceCodeLocator};
fn extract_range(body: &[&Stmt]) -> Range {
@@ -62,9 +62,9 @@ pub fn check_imports(
let has_trailing_content = match_trailing_content(&body, locator);
// Generate the sorted import block.
let expected = sort_imports(
let expected = format_imports(
body,
&settings.line_length,
&(settings.line_length - indentation.len()),
&settings.src,
&settings.isort.known_first_party,
&settings.isort.known_third_party,

View File

@@ -2,5 +2,21 @@
source: src/isort/mod.rs
expression: checks
---
[]
- kind: UnsortedImports
location:
row: 7
column: 0
end_location:
row: 15
column: 0
fix:
patch:
content: " from line_with_88 import aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n from line_with_89 import (\n aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,\n )\n from line_with_90 import (\n aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,\n )\n from line_with_91 import (\n aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,\n )\n from line_with_92 import (\n aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,\n )\n from line_with_93 import (\n aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,\n )\n"
location:
row: 7
column: 0
end_location:
row: 15
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 13
column: 0
fix:
patch:
content: "import glob\nimport os\nimport shutil\nimport tempfile\nimport time\nfrom subprocess import PIPE, STDOUT, Popen\n\nimport BAR\nimport bar\nimport FOO\nimport foo\nimport StringIO\nfrom module import BASIC, CONSTANT, Apple, Class, function\n"
location:
row: 1
column: 0
end_location:
row: 13
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 5
column: 0
fix:
patch:
content: "import os\nimport sys\n\nimport leading_prefix\n\nfrom . import leading_prefix\n"
location:
row: 1
column: 0
end_location:
row: 5
column: 0
applied: false

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

41
src/isort/sorting.rs Normal file
View File

@@ -0,0 +1,41 @@
/// See: https://github.com/PyCQA/isort/blob/12cc5fbd67eebf92eb2213b03c07b138ae1fb448/isort/sorting.py#L13
use crate::python::string;
#[derive(PartialOrd, Ord, PartialEq, Eq)]
pub enum Prefix {
Constants,
Classes,
Variables,
}
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<'a>(
name: &'a str,
asname: &'a Option<String>,
) -> (Prefix, String, &'a Option<String>) {
(
if name.len() > 1 && string::is_upper(name) {
// Ex) `CONSTANT`
Prefix::Constants
} else if name
.chars()
.next()
.map(|char| char.is_uppercase())
.unwrap_or(false)
{
// Ex) `Class`
Prefix::Classes
} else {
// Ex) `variable`
Prefix::Variables
},
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,8 +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> {
pub import: Vec<AliasData<'a>>,
pub import_from: Vec<(ImportFromData<'a>, Vec<AliasData<'a>>)>,
}

View File

@@ -27,8 +27,10 @@ pub mod code_gen;
mod cst;
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;

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