Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
695b06ba60 | ||
|
|
3a2e6926d4 | ||
|
|
d16c3a1186 | ||
|
|
53a2187f02 | ||
|
|
00b5d1059c | ||
|
|
b7acf76aaf | ||
|
|
8cfc0e5cf5 | ||
|
|
aa7681f9ad | ||
|
|
2493d48725 | ||
|
|
1b422a7f12 | ||
|
|
da051624e4 | ||
|
|
da9ae6a42a | ||
|
|
afa59d78bb | ||
|
|
bbc38fea73 | ||
|
|
6bcc11a90f | ||
|
|
6f36e5dd25 | ||
|
|
1d13752eb1 | ||
|
|
394af0dcff | ||
|
|
51cee471a0 | ||
|
|
8df3a5437a | ||
|
|
a21fe716f2 | ||
|
|
558883299a | ||
|
|
048a13c795 | ||
|
|
5a8b7c1d20 | ||
|
|
f8932ec12b | ||
|
|
2e7878ff48 | ||
|
|
5113ded22a | ||
|
|
bf7bf7aa17 | ||
|
|
560c00ff9d | ||
|
|
befe64a10e | ||
|
|
4eccfdeb69 | ||
|
|
4123ba9851 | ||
|
|
e727c24f79 | ||
|
|
bd3b40688f | ||
|
|
b5549382a7 | ||
|
|
8cf745045f | ||
|
|
f6992cc98c | ||
|
|
3cc74c0564 | ||
|
|
887b9aa840 | ||
|
|
faf8556a5c | ||
|
|
1888f6d41b |
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.109
|
||||
rev: v0.0.115
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
||||
35
Cargo.lock
generated
35
Cargo.lock
generated
@@ -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,7 +930,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.109-dev.0"
|
||||
version = "0.0.115-dev.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.22",
|
||||
@@ -1622,6 +1619,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "5.1.2"
|
||||
@@ -2234,11 +2237,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.109"
|
||||
version = "0.0.115"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"bincode",
|
||||
"bitflags",
|
||||
"cacache",
|
||||
"chrono",
|
||||
"clap 4.0.22",
|
||||
@@ -2249,12 +2253,14 @@ dependencies = [
|
||||
"dirs 4.0.0",
|
||||
"fern",
|
||||
"filetime",
|
||||
"fnv",
|
||||
"getrandom 0.2.8",
|
||||
"glob",
|
||||
"insta",
|
||||
"itertools",
|
||||
"libcst",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"notify",
|
||||
"num-bigint",
|
||||
"once_cell",
|
||||
@@ -2279,7 +2285,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.109"
|
||||
version = "0.0.115"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.22",
|
||||
@@ -2773,17 +2779,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"
|
||||
@@ -3070,12 +3065,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"
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -6,7 +6,7 @@ members = [
|
||||
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.109"
|
||||
version = "0.0.115"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -15,24 +15,27 @@ name = "ruff"
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.66" }
|
||||
bincode = { version = "1.3.3" }
|
||||
chrono = { version = "0.4.21" }
|
||||
bitflags = { version = "1.3.2" }
|
||||
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" }
|
||||
log = { version = "0.4.17" }
|
||||
nohash-hasher = { version = "0.2.0" }
|
||||
notify = { version = "4.0.17" }
|
||||
num-bigint = { version = "0.4.3" }
|
||||
once_cell = { version = "1.16.0" }
|
||||
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
|
||||
rayon = { version = "1.5.3" }
|
||||
regex = { version = "1.6.0" }
|
||||
ropey = { version = "1.5.0" }
|
||||
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
|
||||
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "27bf82a2251d7e6ac6cd75e6ad51be12a53d84bb" }
|
||||
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "27bf82a2251d7e6ac6cd75e6ad51be12a53d84bb" }
|
||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "27bf82a2251d7e6ac6cd75e6ad51be12a53d84bb" }
|
||||
@@ -43,7 +46,7 @@ strum_macros = { version = "0.24.3" }
|
||||
textwrap = { version = "0.16.0" }
|
||||
titlecase = { version = "2.2.1" }
|
||||
toml = { version = "0.5.9" }
|
||||
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true }
|
||||
update-informer = { version = "0.5.0", default-features = false, features = ["pypi"], optional = true }
|
||||
walkdir = { version = "2.3.2" }
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
|
||||
114
README.md
114
README.md
@@ -27,9 +27,10 @@ An extremely fast Python linter, written in Rust.
|
||||
|
||||
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
|
||||
functionality behind a single, common interface. Ruff can be used to replace Flake8 (plus a variety
|
||||
of plugins), [`pydocstyle`](https://pypi.org/project/pydocstyle/), [`yesqa`](https://github.com/asottile/yesqa),
|
||||
and even a subset of [`pyupgrade`](https://pypi.org/project/pyupgrade/) and [`autoflake`](https://pypi.org/project/autoflake/)
|
||||
all while executing tens or hundreds of times faster than any individual tool.
|
||||
of plugins), [`isort`](https://pypi.org/project/isort/), [`pydocstyle`](https://pypi.org/project/pydocstyle/),
|
||||
[`yesqa`](https://github.com/asottile/yesqa), and even a subset of [`pyupgrade`](https://pypi.org/project/pyupgrade/)
|
||||
and [`autoflake`](https://pypi.org/project/autoflake/) all while executing tens or hundreds of times
|
||||
faster than any individual tool.
|
||||
|
||||
(Coming from Flake8? Try [`flake8-to-ruff`](https://pypi.org/project/flake8-to-ruff/) to
|
||||
automatically convert your existing configuration.)
|
||||
@@ -56,8 +57,9 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
|
||||
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)
|
||||
12. [flake8-2020](#flake8-2020)
|
||||
13. [Ruff-specific rules](#ruff-specific-rules)
|
||||
14. [Meta rules](#meta-rules)
|
||||
5. [Editor Integrations](#editor-integrations)
|
||||
6. [FAQ](#faq)
|
||||
7. [Development](#development)
|
||||
@@ -97,7 +99,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.109
|
||||
rev: v0.0.115
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -285,16 +287,16 @@ Ruff supports several workflows to aid in `noqa` management.
|
||||
|
||||
First, Ruff provides a special error code, `M001`, to enforce that your `noqa` directives are
|
||||
"valid", in that the errors they _say_ they ignore are actually being triggered on that line (and
|
||||
thus suppressed). **You can run `ruff /path/to/file.py --extend-select M001` to flag unused `noqa`
|
||||
directives.**
|
||||
thus suppressed). You can run `ruff /path/to/file.py --extend-select M001` to flag unused `noqa`
|
||||
directives.
|
||||
|
||||
Second, Ruff can _automatically remove_ unused `noqa` directives via its autofix functionality.
|
||||
**You can run `ruff /path/to/file.py --extend-select M001 --fix` to automatically remove unused
|
||||
`noqa` directives.**
|
||||
You can run `ruff /path/to/file.py --extend-select M001 --fix` to automatically remove unused
|
||||
`noqa` directives.
|
||||
|
||||
Third, Ruff can _automatically add_ `noqa` directives to all failing lines. This is useful when
|
||||
migrating a new codebase to Ruff. **You can run `ruff /path/to/file.py --add-noqa` to automatically
|
||||
add `noqa` directives to all failing lines, with the appropriate error codes.**
|
||||
migrating a new codebase to Ruff. You can run `ruff /path/to/file.py --add-noqa` to automatically
|
||||
add `noqa` directives to all failing lines, with the appropriate error codes.
|
||||
|
||||
## Supported Rules
|
||||
|
||||
@@ -365,6 +367,14 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
|
||||
| W292 | NoNewLineAtEndOfFile | No newline at end of file | |
|
||||
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | |
|
||||
|
||||
### isort
|
||||
|
||||
For more, see [isort](https://pypi.org/project/isort/5.10.1/) on PyPI.
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| I001 | UnsortedImports | Import block is un-sorted or un-formatted | 🛠 |
|
||||
|
||||
### pydocstyle
|
||||
|
||||
For more, see [pydocstyle](https://pypi.org/project/pydocstyle/6.1.1/) on PyPI.
|
||||
@@ -431,8 +441,9 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
|
||||
| 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 | 🛠 |
|
||||
| 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
|
||||
|
||||
@@ -456,6 +467,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.
|
||||
@@ -492,6 +516,8 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
|
||||
| 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. | |
|
||||
| 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,):`. | |
|
||||
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 |
|
||||
@@ -499,6 +525,7 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
|
||||
| B016 | CannotRaiseLiteral | Cannot raise a literal. Did you intend to return it or raise an Exception? | |
|
||||
| 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. | |
|
||||
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
|
||||
| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged. | |
|
||||
|
||||
@@ -550,6 +577,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 |
|
||||
@@ -655,8 +699,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/) (18/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/) (21/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:
|
||||
@@ -677,11 +723,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/) (18/32)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (21/32)
|
||||
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
|
||||
|
||||
Ruff also implements the functionality that you get from [`yesqa`](https://github.com/asottile/yesqa),
|
||||
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (14/34).
|
||||
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/) (15/34).
|
||||
|
||||
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.
|
||||
|
||||
@@ -701,6 +749,34 @@ on Rust at all.
|
||||
Ruff does not yet support third-party plugins, though a plugin system is within-scope for the
|
||||
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`).
|
||||
|
||||
Like `isort`, Ruff's import sorting is compatible with Black.
|
||||
|
||||
Ruff is less configurable than `isort`, but supports the `known-first-party`, `known-third-party`,
|
||||
`extra-standard-library`, and `src` settings, like so:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
select = [
|
||||
# Pyflakes
|
||||
"F",
|
||||
# Pycodestyle
|
||||
"E",
|
||||
"W",
|
||||
# isort
|
||||
"I"
|
||||
]
|
||||
src = ["src", "tests"]
|
||||
|
||||
[tool.ruff.isort]
|
||||
known-first-party = ["my_module1", "my_module2"]
|
||||
```
|
||||
|
||||
### Does Ruff support NumPy- or Google-style docstrings?
|
||||
|
||||
Yes! To enable a specific docstring convention, start by enabling all `pydocstyle` error codes, and
|
||||
|
||||
4
flake8_to_ruff/Cargo.lock
generated
4
flake8_to_ruff/Cargo.lock
generated
@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8_to_ruff"
|
||||
version = "0.0.109"
|
||||
version = "0.0.115"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1975,7 +1975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.109"
|
||||
version = "0.0.115"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.109-dev.0"
|
||||
version = "0.0.115-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -208,6 +220,7 @@ mod tests {
|
||||
let actual = convert(&HashMap::from([]), None)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
line_length: None,
|
||||
src: None,
|
||||
fix: None,
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
@@ -223,7 +236,9 @@ mod tests {
|
||||
dummy_variable_rgx: None,
|
||||
target_version: None,
|
||||
flake8_annotations: None,
|
||||
flake8_bugbear: None,
|
||||
flake8_quotes: None,
|
||||
isort: None,
|
||||
pep8_naming: None,
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
@@ -239,6 +254,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
line_length: Some(100),
|
||||
src: None,
|
||||
fix: None,
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
@@ -254,7 +270,9 @@ mod tests {
|
||||
dummy_variable_rgx: None,
|
||||
target_version: None,
|
||||
flake8_annotations: None,
|
||||
flake8_bugbear: None,
|
||||
flake8_quotes: None,
|
||||
isort: None,
|
||||
pep8_naming: None,
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
@@ -270,6 +288,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
line_length: Some(100),
|
||||
src: None,
|
||||
fix: None,
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
@@ -285,7 +304,9 @@ mod tests {
|
||||
dummy_variable_rgx: None,
|
||||
target_version: None,
|
||||
flake8_annotations: None,
|
||||
flake8_bugbear: None,
|
||||
flake8_quotes: None,
|
||||
isort: None,
|
||||
pep8_naming: None,
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
@@ -301,6 +322,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
line_length: None,
|
||||
src: None,
|
||||
fix: None,
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
@@ -316,7 +338,9 @@ mod tests {
|
||||
dummy_variable_rgx: None,
|
||||
target_version: None,
|
||||
flake8_annotations: None,
|
||||
flake8_bugbear: None,
|
||||
flake8_quotes: None,
|
||||
isort: None,
|
||||
pep8_naming: None,
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
@@ -332,6 +356,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
line_length: None,
|
||||
src: None,
|
||||
fix: None,
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
@@ -347,12 +372,14 @@ 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,
|
||||
docstring_quotes: None,
|
||||
avoid_escape: None,
|
||||
}),
|
||||
isort: None,
|
||||
pep8_naming: None,
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
@@ -371,6 +398,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
line_length: None,
|
||||
src: None,
|
||||
fix: None,
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
@@ -421,7 +449,9 @@ mod tests {
|
||||
dummy_variable_rgx: None,
|
||||
target_version: None,
|
||||
flake8_annotations: None,
|
||||
flake8_bugbear: None,
|
||||
flake8_quotes: None,
|
||||
isort: None,
|
||||
pep8_naming: None,
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
@@ -437,6 +467,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
line_length: None,
|
||||
src: None,
|
||||
fix: None,
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
@@ -453,12 +484,14 @@ 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,
|
||||
docstring_quotes: None,
|
||||
avoid_escape: None,
|
||||
}),
|
||||
isort: None,
|
||||
pep8_naming: None,
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -32,3 +32,7 @@ build-backend = "maturin"
|
||||
bindings = "bin"
|
||||
sdist-include = ["Cargo.lock"]
|
||||
strip = true
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
known_third_party = ["fastapi", "pydantic", "starlette"]
|
||||
|
||||
20
resources/test/fixtures/B008_extended.py
vendored
Normal file
20
resources/test/fixtures/B008_extended.py
vendored
Normal 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)):
|
||||
...
|
||||
36
resources/test/fixtures/B009_B010.py
vendored
Normal file
36
resources/test/fixtures/B009_B010.py
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Should emit:
|
||||
B009 - Line 18, 19, 20, 21, 22
|
||||
B010 - Line 33, 34, 35, 36
|
||||
"""
|
||||
|
||||
# Valid getattr usage
|
||||
getattr(foo, bar)
|
||||
getattr(foo, "bar", None)
|
||||
getattr(foo, "bar{foo}".format(foo="a"), None)
|
||||
getattr(foo, "bar{foo}".format(foo="a"))
|
||||
getattr(foo, bar, None)
|
||||
getattr(foo, "123abc")
|
||||
getattr(foo, r"123\abc")
|
||||
getattr(foo, "except")
|
||||
|
||||
# Invalid usage
|
||||
getattr(foo, "bar")
|
||||
getattr(foo, "_123abc")
|
||||
getattr(foo, "abc123")
|
||||
getattr(foo, r"abc123")
|
||||
_ = lambda x: getattr(x, "bar")
|
||||
|
||||
# Valid setattr usage
|
||||
setattr(foo, bar, None)
|
||||
setattr(foo, "bar{foo}".format(foo="a"), None)
|
||||
setattr(foo, "123abc", None)
|
||||
setattr(foo, r"123\abc", None)
|
||||
setattr(foo, "except", None)
|
||||
_ = lambda x: setattr(x, "bar", 1)
|
||||
|
||||
# Invalid usage
|
||||
setattr(foo, "bar", None)
|
||||
setattr(foo, "_123abc", None)
|
||||
setattr(foo, "abc123", None)
|
||||
setattr(foo, r"abc123", None)
|
||||
108
resources/test/fixtures/B019.py
vendored
Normal file
108
resources/test/fixtures/B019.py
vendored
Normal 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):
|
||||
...
|
||||
1
resources/test/fixtures/F401_0.py
vendored
1
resources/test/fixtures/F401_0.py
vendored
@@ -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
|
||||
|
||||
11
resources/test/fixtures/S101.py
vendored
Normal file
11
resources/test/fixtures/S101.py
vendored
Normal 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
5
resources/test/fixtures/S102.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
def fn():
|
||||
# Error
|
||||
exec('x = 2')
|
||||
|
||||
exec('y = 3')
|
||||
19
resources/test/fixtures/S104.py
vendored
Normal file
19
resources/test/fixtures/S104.py
vendored
Normal 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
53
resources/test/fixtures/S105.py
vendored
Normal 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
13
resources/test/fixtures/S106.py
vendored
Normal 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
30
resources/test/fixtures/S107.py
vendored
Normal 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
|
||||
15
resources/test/fixtures/U010.py
vendored
15
resources/test/fixtures/U010.py
vendored
@@ -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
52
resources/test/fixtures/U012.py
vendored
Normal 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")
|
||||
13
resources/test/fixtures/YTT101.py
vendored
Normal file
13
resources/test/fixtures/YTT101.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
from sys import version, version as v
|
||||
|
||||
print(sys.version)
|
||||
|
||||
print(sys.version[:3])
|
||||
print(version[:3])
|
||||
|
||||
# ignore from imports with aliases, patches welcome
|
||||
print(v[:3])
|
||||
# the tool is timid and only flags certain numeric slices
|
||||
i = 3
|
||||
print(sys.version[:i])
|
||||
5
resources/test/fixtures/YTT102.py
vendored
Normal file
5
resources/test/fixtures/YTT102.py
vendored
Normal 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
8
resources/test/fixtures/YTT103.py
vendored
Normal 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
10
resources/test/fixtures/YTT201.py
vendored
Normal 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
7
resources/test/fixtures/YTT202.py
vendored
Normal 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
5
resources/test/fixtures/YTT203.py
vendored
Normal 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
5
resources/test/fixtures/YTT204.py
vendored
Normal 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
5
resources/test/fixtures/YTT301.py
vendored
Normal 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
8
resources/test/fixtures/YTT302.py
vendored
Normal 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
5
resources/test/fixtures/YTT303.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import sys
|
||||
from sys import version
|
||||
|
||||
print(sys.version[:1])
|
||||
print(version[:1])
|
||||
9
resources/test/fixtures/excluded.py
vendored
9
resources/test/fixtures/excluded.py
vendored
@@ -1,9 +0,0 @@
|
||||
a = "abc"
|
||||
b = f"ghi{'jkl'}"
|
||||
|
||||
c = f"def"
|
||||
d = f"def" + "ghi"
|
||||
e = (
|
||||
f"def" +
|
||||
"ghi"
|
||||
)
|
||||
5
resources/test/fixtures/isort/combine_import_froms.py
vendored
Normal file
5
resources/test/fixtures/isort/combine_import_froms.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
from collections import Awaitable
|
||||
from collections import AsyncIterable
|
||||
from collections import Collection
|
||||
from collections import ChainMap
|
||||
from collections import MutableSequence, MutableMapping
|
||||
4
resources/test/fixtures/isort/deduplicate_imports.py
vendored
Normal file
4
resources/test/fixtures/isort/deduplicate_imports.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import os
|
||||
import os
|
||||
import os as os1
|
||||
import os as os2
|
||||
14
resources/test/fixtures/isort/fit_line_length.py
vendored
Normal file
14
resources/test/fixtures/isort/fit_line_length.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
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,
|
||||
)
|
||||
2
resources/test/fixtures/isort/import_from_after_import.py
vendored
Normal file
2
resources/test/fixtures/isort/import_from_after_import.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
from collections import Collection
|
||||
import os
|
||||
6
resources/test/fixtures/isort/leading_prefix.py
vendored
Normal file
6
resources/test/fixtures/isort/leading_prefix.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
x = 1; import sys
|
||||
import os
|
||||
|
||||
if True:
|
||||
x = 1; import sys
|
||||
import os
|
||||
3
resources/test/fixtures/isort/no_reorder_within_section.py
vendored
Normal file
3
resources/test/fixtures/isort/no_reorder_within_section.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# OK
|
||||
import os
|
||||
import sys
|
||||
12
resources/test/fixtures/isort/order_by_type.py
vendored
Normal file
12
resources/test/fixtures/isort/order_by_type.py
vendored
Normal 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
|
||||
6
resources/test/fixtures/isort/preserve_indentation.py
vendored
Normal file
6
resources/test/fixtures/isort/preserve_indentation.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
if True:
|
||||
import sys
|
||||
import os
|
||||
else:
|
||||
import sys
|
||||
import os
|
||||
2
resources/test/fixtures/isort/pyproject.toml
vendored
Normal file
2
resources/test/fixtures/isort/pyproject.toml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
2
resources/test/fixtures/isort/reorder_within_section.py
vendored
Normal file
2
resources/test/fixtures/isort/reorder_within_section.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import sys
|
||||
import os
|
||||
5
resources/test/fixtures/isort/separate_first_party_imports.py
vendored
Normal file
5
resources/test/fixtures/isort/separate_first_party_imports.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import sys
|
||||
import leading_prefix
|
||||
import numpy as np
|
||||
import os
|
||||
from leading_prefix import Class
|
||||
3
resources/test/fixtures/isort/separate_future_imports.py
vendored
Normal file
3
resources/test/fixtures/isort/separate_future_imports.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import sys
|
||||
import os
|
||||
from __future__ import annotations
|
||||
4
resources/test/fixtures/isort/separate_local_folder_imports.py
vendored
Normal file
4
resources/test/fixtures/isort/separate_local_folder_imports.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import sys
|
||||
import leading_prefix
|
||||
import os
|
||||
from . import leading_prefix
|
||||
4
resources/test/fixtures/isort/separate_third_party_imports.py
vendored
Normal file
4
resources/test/fixtures/isort/separate_third_party_imports.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import pandas as pd
|
||||
import sys
|
||||
import numpy as np
|
||||
import os
|
||||
10
resources/test/fixtures/isort/skip.py
vendored
Normal file
10
resources/test/fixtures/isort/skip.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# isort: off
|
||||
import sys
|
||||
import os
|
||||
import collections
|
||||
# isort: on
|
||||
|
||||
import sys
|
||||
import os # isort: skip
|
||||
import collections
|
||||
import abc
|
||||
6
resources/test/fixtures/isort/trailing_suffix.py
vendored
Normal file
6
resources/test/fixtures/isort/trailing_suffix.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import sys
|
||||
import os; x = 1
|
||||
|
||||
if True:
|
||||
import sys
|
||||
import os; x = 1
|
||||
7
resources/test/fixtures/pyproject.toml
vendored
7
resources/test/fixtures/pyproject.toml
vendored
@@ -1,12 +1,15 @@
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
extend-exclude = [
|
||||
"excluded.py",
|
||||
"excluded_file.py",
|
||||
"migrations",
|
||||
"directory/also_excluded.py",
|
||||
"with_excluded_file/other_excluded_file.py",
|
||||
]
|
||||
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"
|
||||
|
||||
7
ruff/__main__.py
Normal file
7
ruff/__main__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
if __name__ == "__main__":
|
||||
ruff = os.path.join(sysconfig.get_path("scripts"), "ruff")
|
||||
os.spawnv(os.P_WAIT, ruff, [ruff, *sys.argv[1:]])
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.109"
|
||||
version = "0.0.115"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use fnv::FnvHashSet;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, StmtKind};
|
||||
@@ -47,7 +46,7 @@ pub fn match_name_or_attr_from_module(
|
||||
expr: &Expr,
|
||||
target: &str,
|
||||
module: &str,
|
||||
imports: Option<&BTreeSet<&str>>,
|
||||
imports: Option<&FnvHashSet<&str>>,
|
||||
) -> bool {
|
||||
match &expr.node {
|
||||
ExprKind::Attribute { value, attr, .. } => match &value.node {
|
||||
|
||||
@@ -46,6 +46,7 @@ pub enum ScopeKind<'a> {
|
||||
Generator,
|
||||
Module,
|
||||
Arg,
|
||||
Lambda,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
240
src/check_ast.rs
240
src/check_ast.rs
@@ -1,9 +1,10 @@
|
||||
//! 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 log::error;
|
||||
use rustpython_parser::ast::{
|
||||
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
|
||||
@@ -32,23 +33,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 +51,9 @@ 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>>,
|
||||
// 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>,
|
||||
@@ -115,6 +104,7 @@ impl<'a> Checker<'a> {
|
||||
deferred_functions: Default::default(),
|
||||
deferred_lambdas: Default::default(),
|
||||
deferred_assignments: Default::default(),
|
||||
// Internal, derivative state.
|
||||
visible_scope: VisibleScope {
|
||||
modifier: Modifier::Module,
|
||||
visibility: module_visibility(path),
|
||||
@@ -343,6 +333,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);
|
||||
|
||||
@@ -397,14 +396,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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -577,17 +576,15 @@ 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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,6 +597,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);
|
||||
@@ -632,14 +635,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(
|
||||
@@ -799,6 +794,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) {
|
||||
@@ -839,6 +837,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) {
|
||||
@@ -983,6 +988,14 @@ where
|
||||
if self.match_typing_module(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) {
|
||||
@@ -1000,34 +1013,40 @@ 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.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);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -1036,6 +1055,10 @@ where
|
||||
{
|
||||
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,
|
||||
@@ -1051,6 +1074,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)
|
||||
@@ -1064,11 +1091,34 @@ where
|
||||
if self.settings.enabled.contains(&CheckCode::B005) {
|
||||
flake8_bugbear::plugins::strip_with_multi_characters(self, expr, func, args);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::B009) {
|
||||
flake8_bugbear::plugins::getattr_with_constant(self, expr, func, args);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::B010) {
|
||||
if !self
|
||||
.scope_stack
|
||||
.iter()
|
||||
.rev()
|
||||
.any(|index| matches!(self.scopes[*index].kind, ScopeKind::Lambda))
|
||||
{
|
||||
flake8_bugbear::plugins::setattr_with_constant(self, expr, func, 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) {
|
||||
@@ -1408,6 +1458,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),
|
||||
@@ -1417,6 +1486,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.
|
||||
@@ -1451,8 +1528,8 @@ where
|
||||
for expr in &args.defaults {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
self.push_scope(Scope::new(ScopeKind::Lambda))
|
||||
}
|
||||
|
||||
ExprKind::ListComp { elt, generators } | ExprKind::SetComp { elt, generators } => {
|
||||
if self.settings.enabled.contains(&CheckCode::C416) {
|
||||
if let Some(check) = flake8_comprehensions::checks::unnecessary_comprehension(
|
||||
@@ -1468,7 +1545,6 @@ where
|
||||
}
|
||||
self.push_scope(Scope::new(ScopeKind::Generator))
|
||||
}
|
||||
|
||||
ExprKind::GeneratorExp { .. } | ExprKind::DictComp { .. } => {
|
||||
self.push_scope(Scope::new(ScopeKind::Generator))
|
||||
}
|
||||
@@ -1639,7 +1715,8 @@ where
|
||||
|
||||
// Post-visit.
|
||||
match &expr.node {
|
||||
ExprKind::GeneratorExp { .. }
|
||||
ExprKind::Lambda { .. }
|
||||
| ExprKind::GeneratorExp { .. }
|
||||
| ExprKind::ListComp { .. }
|
||||
| ExprKind::DictComp { .. }
|
||||
| ExprKind::SetComp { .. } => {
|
||||
@@ -2231,7 +2308,7 @@ impl<'a> Checker<'a> {
|
||||
while let Some((expr, scopes, parents)) = self.deferred_lambdas.pop() {
|
||||
self.parent_stack = parents;
|
||||
self.scope_stack = scopes;
|
||||
self.push_scope(Scope::new(ScopeKind::Function(Default::default())));
|
||||
self.push_scope(Scope::new(ScopeKind::Lambda));
|
||||
|
||||
if let ExprKind::Lambda { args, body } = &expr.node {
|
||||
self.visit_arguments(args);
|
||||
@@ -2367,16 +2444,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
|
||||
}
|
||||
}
|
||||
@@ -2588,5 +2669,8 @@ pub fn check_ast(
|
||||
// Check docstrings.
|
||||
checker.check_definitions();
|
||||
|
||||
// Check import blocks.
|
||||
// checker.check_import_blocks();
|
||||
|
||||
checker.checks
|
||||
}
|
||||
|
||||
43
src/check_imports.rs
Normal file
43
src/check_imports.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
//! Lint rules based on import analysis.
|
||||
|
||||
use nohash_hasher::IntSet;
|
||||
use rustpython_parser::ast::Suite;
|
||||
|
||||
use crate::ast::visitor::Visitor;
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::Check;
|
||||
use crate::isort;
|
||||
use crate::isort::track::ImportTracker;
|
||||
use crate::settings::Settings;
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
|
||||
fn check_import_blocks(
|
||||
tracker: ImportTracker,
|
||||
locator: &SourceCodeLocator,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Vec<Check> {
|
||||
let mut checks = vec![];
|
||||
for block in tracker.into_iter() {
|
||||
if !block.is_empty() {
|
||||
if let Some(check) = isort::plugins::check_imports(block, locator, settings, autofix) {
|
||||
checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
checks
|
||||
}
|
||||
|
||||
pub fn check_imports(
|
||||
python_ast: &Suite,
|
||||
locator: &SourceCodeLocator,
|
||||
exclusions: &IntSet<usize>,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Vec<Check> {
|
||||
let mut tracker = ImportTracker::new(exclusions);
|
||||
for stmt in python_ast {
|
||||
tracker.visit_stmt(stmt);
|
||||
}
|
||||
check_import_blocks(tracker, locator, settings, autofix)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Lint rules based on checking raw physical lines.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use nohash_hasher::IntMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_parser::ast::Location;
|
||||
@@ -36,7 +35,7 @@ fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
|
||||
pub fn check_lines(
|
||||
checks: &mut Vec<Check>,
|
||||
contents: &str,
|
||||
noqa_line_for: &[usize],
|
||||
noqa_line_for: &IntMap<usize, usize>,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) {
|
||||
@@ -44,18 +43,23 @@ pub fn check_lines(
|
||||
let enforce_line_too_long = settings.enabled.contains(&CheckCode::E501);
|
||||
let enforce_noqa = settings.enabled.contains(&CheckCode::M001);
|
||||
|
||||
let mut noqa_directives: BTreeMap<usize, (Directive, Vec<&str>)> = BTreeMap::new();
|
||||
|
||||
let mut noqa_directives: IntMap<usize, (Directive, Vec<&str>)> = IntMap::default();
|
||||
let mut line_checks = vec![];
|
||||
let mut ignored = vec![];
|
||||
|
||||
checks.sort_by_key(|check| check.location);
|
||||
let mut checks_iter = checks.iter().enumerate().peekable();
|
||||
if let Some((_index, check)) = checks_iter.peek() {
|
||||
assert!(check.location.row() >= 1);
|
||||
}
|
||||
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
for (lineno, line) in lines.iter().enumerate() {
|
||||
// Grab the noqa (logical) line number for the current (physical) line.
|
||||
// If there are newlines at the end of the file, they won't be represented in
|
||||
// `noqa_line_for`, so fallback to the current line.
|
||||
let noqa_lineno = noqa_line_for
|
||||
.get(lineno)
|
||||
.get(&lineno)
|
||||
.map(|lineno| lineno - 1)
|
||||
.unwrap_or(lineno);
|
||||
|
||||
@@ -90,26 +94,25 @@ pub fn check_lines(
|
||||
}
|
||||
|
||||
// Remove any ignored checks.
|
||||
// TODO(charlie): Only validate checks for the current line.
|
||||
for (index, check) in checks.iter().enumerate() {
|
||||
if check.location.row() == lineno + 1 {
|
||||
let noqa = noqa_directives
|
||||
.entry(noqa_lineno)
|
||||
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
||||
while let Some((index, check)) =
|
||||
checks_iter.next_if(|(_index, check)| check.location.row() == lineno + 1)
|
||||
{
|
||||
let noqa = noqa_directives
|
||||
.entry(noqa_lineno)
|
||||
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
||||
|
||||
match noqa {
|
||||
(Directive::All(..), matches) => {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
ignored.push(index)
|
||||
}
|
||||
(Directive::Codes(_, _, codes), matches) => {
|
||||
if codes.contains(&check.kind.code().as_ref()) {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
ignored.push(index);
|
||||
}
|
||||
}
|
||||
(Directive::None, _) => {}
|
||||
match noqa {
|
||||
(Directive::All(..), matches) => {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
ignored.push(index)
|
||||
}
|
||||
(Directive::Codes(_, _, codes), matches) => {
|
||||
if codes.contains(&check.kind.code().as_ref()) {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
ignored.push(index);
|
||||
}
|
||||
}
|
||||
(Directive::None, _) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +156,7 @@ pub fn check_lines(
|
||||
if let Some(line) = lines.last() {
|
||||
let lineno = lines.len() - 1;
|
||||
let noqa_lineno = noqa_line_for
|
||||
.get(lineno)
|
||||
.get(&lineno)
|
||||
.map(|lineno| lineno - 1)
|
||||
.unwrap_or(lineno);
|
||||
|
||||
@@ -257,6 +260,8 @@ pub fn check_lines(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nohash_hasher::IntMap;
|
||||
|
||||
use super::check_lines;
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
@@ -265,7 +270,7 @@ mod tests {
|
||||
#[test]
|
||||
fn e501_non_ascii_char() {
|
||||
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
|
||||
let noqa_line_for: Vec<usize> = vec![1];
|
||||
let noqa_line_for: IntMap<usize, usize> = Default::default();
|
||||
let check_with_max_line_length = |line_length: usize| {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
check_lines(
|
||||
|
||||
211
src/checks.rs
211
src/checks.rs
@@ -84,6 +84,8 @@ pub enum CheckCode {
|
||||
B006,
|
||||
B007,
|
||||
B008,
|
||||
B009,
|
||||
B010,
|
||||
B011,
|
||||
B013,
|
||||
B014,
|
||||
@@ -91,6 +93,7 @@ pub enum CheckCode {
|
||||
B016,
|
||||
B017,
|
||||
B018,
|
||||
B019,
|
||||
B025,
|
||||
B026,
|
||||
// flake8-comprehensions
|
||||
@@ -130,6 +133,17 @@ pub enum CheckCode {
|
||||
ANN205,
|
||||
ANN206,
|
||||
ANN401,
|
||||
// flake8-2020
|
||||
YTT101,
|
||||
YTT102,
|
||||
YTT103,
|
||||
YTT201,
|
||||
YTT202,
|
||||
YTT203,
|
||||
YTT204,
|
||||
YTT301,
|
||||
YTT302,
|
||||
YTT303,
|
||||
// pyupgrade
|
||||
U001,
|
||||
U002,
|
||||
@@ -142,6 +156,7 @@ pub enum CheckCode {
|
||||
U009,
|
||||
U010,
|
||||
U011,
|
||||
U012,
|
||||
// pydocstyle
|
||||
D100,
|
||||
D101,
|
||||
@@ -203,6 +218,15 @@ pub enum CheckCode {
|
||||
N816,
|
||||
N817,
|
||||
N818,
|
||||
// isort
|
||||
I001,
|
||||
// flake8-bandit
|
||||
S101,
|
||||
S102,
|
||||
S104,
|
||||
S105,
|
||||
S106,
|
||||
S107,
|
||||
// Ruff
|
||||
RUF001,
|
||||
RUF002,
|
||||
@@ -215,15 +239,18 @@ pub enum CheckCode {
|
||||
pub enum CheckCategory {
|
||||
Pyflakes,
|
||||
Pycodestyle,
|
||||
Isort,
|
||||
Pydocstyle,
|
||||
Pyupgrade,
|
||||
PEP8Naming,
|
||||
Flake8Bandit,
|
||||
Flake8Comprehensions,
|
||||
Flake8Bugbear,
|
||||
Flake8Builtins,
|
||||
Flake8Print,
|
||||
Flake8Quotes,
|
||||
Flake8Annotations,
|
||||
Flake82020,
|
||||
Ruff,
|
||||
Meta,
|
||||
}
|
||||
@@ -233,12 +260,15 @@ impl CheckCategory {
|
||||
match self {
|
||||
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",
|
||||
@@ -251,6 +281,7 @@ impl CheckCategory {
|
||||
match self {
|
||||
CheckCategory::Pycodestyle => Some("https://pypi.org/project/pycodestyle/2.9.1/"),
|
||||
CheckCategory::Pyflakes => Some("https://pypi.org/project/pyflakes/2.5.0/"),
|
||||
CheckCategory::Isort => Some("https://pypi.org/project/isort/5.10.1/"),
|
||||
CheckCategory::Flake8Builtins => {
|
||||
Some("https://pypi.org/project/flake8-builtins/2.0.1/")
|
||||
}
|
||||
@@ -265,9 +296,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,
|
||||
}
|
||||
@@ -280,6 +313,7 @@ pub enum LintSource {
|
||||
FileSystem,
|
||||
Lines,
|
||||
Tokens,
|
||||
Imports,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -349,6 +383,8 @@ pub enum CheckKind {
|
||||
MutableArgumentDefault,
|
||||
UnusedLoopControlVariable(String),
|
||||
FunctionCallArgumentDefault,
|
||||
GetAttrWithConstant,
|
||||
SetAttrWithConstant,
|
||||
DoNotAssertFalse,
|
||||
RedundantTupleInExceptionHandler(String),
|
||||
DuplicateHandlerException(Vec<String>),
|
||||
@@ -356,6 +392,7 @@ pub enum CheckKind {
|
||||
CannotRaiseLiteral,
|
||||
NoAssertRaisesException,
|
||||
UselessExpression,
|
||||
CachedInstanceMethod,
|
||||
DuplicateTryBlockException(String),
|
||||
StarArgUnpackingAfterKeywordArg,
|
||||
// flake8-comprehensions
|
||||
@@ -395,6 +432,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,
|
||||
@@ -405,8 +453,9 @@ pub enum CheckKind {
|
||||
UsePEP604Annotation,
|
||||
SuperCallWithParameters,
|
||||
PEP3120UnnecessaryCodingComment,
|
||||
UnnecessaryFutureImport(String),
|
||||
UnnecessaryFutureImport(Vec<String>),
|
||||
UnnecessaryLRUCacheParams,
|
||||
UnnecessaryEncodeUTF8,
|
||||
// pydocstyle
|
||||
BlankLineAfterLastSection(String),
|
||||
BlankLineAfterSection(String),
|
||||
@@ -468,6 +517,15 @@ pub enum CheckKind {
|
||||
MixedCaseVariableInGlobalScope(String),
|
||||
CamelcaseImportedAsAcronym(String, String),
|
||||
ErrorSuffixOnExceptionName(String),
|
||||
// isort
|
||||
UnsortedImports,
|
||||
// flake8-bandit
|
||||
AssertUsed,
|
||||
ExecUsed,
|
||||
HardcodedBindAllInterfaces,
|
||||
HardcodedPasswordString(String),
|
||||
HardcodedPasswordFuncArg(String),
|
||||
HardcodedPasswordDefault(String),
|
||||
// Ruff
|
||||
AmbiguousUnicodeCharacterString(char, char),
|
||||
AmbiguousUnicodeCharacterDocstring(char, char),
|
||||
@@ -493,6 +551,7 @@ impl CheckCode {
|
||||
| CheckCode::RUF002
|
||||
| CheckCode::RUF003 => &LintSource::Tokens,
|
||||
CheckCode::E902 => &LintSource::FileSystem,
|
||||
CheckCode::I001 => &LintSource::Imports,
|
||||
_ => &LintSource::AST,
|
||||
}
|
||||
}
|
||||
@@ -561,6 +620,8 @@ impl CheckCode {
|
||||
CheckCode::B006 => CheckKind::MutableArgumentDefault,
|
||||
CheckCode::B007 => CheckKind::UnusedLoopControlVariable("i".to_string()),
|
||||
CheckCode::B008 => CheckKind::FunctionCallArgumentDefault,
|
||||
CheckCode::B009 => CheckKind::GetAttrWithConstant,
|
||||
CheckCode::B010 => CheckKind::SetAttrWithConstant,
|
||||
CheckCode::B011 => CheckKind::DoNotAssertFalse,
|
||||
CheckCode::B013 => {
|
||||
CheckKind::RedundantTupleInExceptionHandler("ValueError".to_string())
|
||||
@@ -570,6 +631,7 @@ impl CheckCode {
|
||||
CheckCode::B016 => CheckKind::CannotRaiseLiteral,
|
||||
CheckCode::B017 => CheckKind::NoAssertRaisesException,
|
||||
CheckCode::B018 => CheckKind::UselessExpression,
|
||||
CheckCode::B019 => CheckKind::CachedInstanceMethod,
|
||||
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
|
||||
CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg,
|
||||
// flake8-comprehensions
|
||||
@@ -622,6 +684,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,
|
||||
@@ -635,8 +708,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,
|
||||
@@ -714,6 +788,15 @@ impl CheckCode {
|
||||
CheckKind::CamelcaseImportedAsAcronym("...".to_string(), "...".to_string())
|
||||
}
|
||||
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'),
|
||||
@@ -779,6 +862,8 @@ impl CheckCode {
|
||||
CheckCode::B006 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B007 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B008 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B009 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B010 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B011 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B013 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B014 => CheckCategory::Flake8Bugbear,
|
||||
@@ -786,6 +871,7 @@ impl CheckCode {
|
||||
CheckCode::B016 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B017 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B018 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B019 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B025 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B026 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
|
||||
@@ -821,6 +907,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,
|
||||
@@ -832,6 +928,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,
|
||||
@@ -891,6 +988,13 @@ impl CheckCode {
|
||||
CheckCode::N816 => CheckCategory::PEP8Naming,
|
||||
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,
|
||||
@@ -961,6 +1065,8 @@ impl CheckKind {
|
||||
CheckKind::MutableArgumentDefault => &CheckCode::B006,
|
||||
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
|
||||
CheckKind::FunctionCallArgumentDefault => &CheckCode::B008,
|
||||
CheckKind::GetAttrWithConstant => &CheckCode::B009,
|
||||
CheckKind::SetAttrWithConstant => &CheckCode::B010,
|
||||
CheckKind::DoNotAssertFalse => &CheckCode::B011,
|
||||
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
|
||||
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
|
||||
@@ -968,6 +1074,7 @@ impl CheckKind {
|
||||
CheckKind::CannotRaiseLiteral => &CheckCode::B016,
|
||||
CheckKind::NoAssertRaisesException => &CheckCode::B017,
|
||||
CheckKind::UselessExpression => &CheckCode::B018,
|
||||
CheckKind::CachedInstanceMethod => &CheckCode::B019,
|
||||
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
|
||||
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
|
||||
// flake8-comprehensions
|
||||
@@ -1007,6 +1114,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,
|
||||
@@ -1019,6 +1137,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,
|
||||
@@ -1080,6 +1199,15 @@ impl CheckKind {
|
||||
CheckKind::MixedCaseVariableInGlobalScope(..) => &CheckCode::N816,
|
||||
CheckKind::CamelcaseImportedAsAcronym(..) => &CheckCode::N817,
|
||||
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,
|
||||
@@ -1264,6 +1392,14 @@ impl CheckKind {
|
||||
CheckKind::FunctionCallArgumentDefault => {
|
||||
"Do not perform function calls in argument defaults.".to_string()
|
||||
}
|
||||
CheckKind::GetAttrWithConstant => "Do not call `getattr` with a constant attribute \
|
||||
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 \
|
||||
access."
|
||||
.to_string(),
|
||||
CheckKind::DoNotAssertFalse => "Do not `assert False` (`python -O` removes these \
|
||||
calls), raise `AssertionError()`"
|
||||
.to_string(),
|
||||
@@ -1299,6 +1435,9 @@ 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::DuplicateTryBlockException(name) => {
|
||||
format!("try-except block with duplicate exception `{name}`")
|
||||
}
|
||||
@@ -1441,6 +1580,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())
|
||||
@@ -1466,12 +1637,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 => {
|
||||
@@ -1635,6 +1813,23 @@ impl CheckKind {
|
||||
CheckKind::PEP3120UnnecessaryCodingComment => {
|
||||
"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!(
|
||||
@@ -1709,6 +1904,7 @@ impl CheckKind {
|
||||
| CheckKind::DeprecatedUnittestAlias(_, _)
|
||||
| CheckKind::DoNotAssertFalse
|
||||
| CheckKind::DuplicateHandlerException(_)
|
||||
| CheckKind::GetAttrWithConstant
|
||||
| CheckKind::IsLiteral
|
||||
| CheckKind::NewLineAfterLastParagraph
|
||||
| CheckKind::NewLineAfterSectionName(_)
|
||||
@@ -1735,17 +1931,20 @@ impl CheckKind {
|
||||
| CheckKind::UnnecessaryAbspath
|
||||
| CheckKind::UnnecessaryCollectionCall(_)
|
||||
| CheckKind::UnnecessaryComprehension(_)
|
||||
| CheckKind::UnnecessaryEncodeUTF8
|
||||
| CheckKind::UnnecessaryFutureImport(_)
|
||||
| CheckKind::UnnecessaryGeneratorDict
|
||||
| CheckKind::UnnecessaryGeneratorList
|
||||
| CheckKind::UnnecessaryGeneratorSet
|
||||
| CheckKind::UnnecessaryLRUCacheParams
|
||||
| CheckKind::UnnecessaryListCall
|
||||
| CheckKind::UnnecessaryListComprehensionSet
|
||||
| CheckKind::UnnecessaryListComprehensionDict
|
||||
| CheckKind::UnnecessaryListComprehensionSet
|
||||
| CheckKind::UnnecessaryLiteralDict(_)
|
||||
| CheckKind::UnnecessaryLiteralSet(_)
|
||||
| CheckKind::UnnecessaryLiteralWithinListCall(_)
|
||||
| CheckKind::UnnecessaryLiteralWithinTupleCall(_)
|
||||
| CheckKind::UnsortedImports
|
||||
| CheckKind::UnusedImport(_, false)
|
||||
| CheckKind::UnusedLoopControlVariable(_)
|
||||
| CheckKind::UnusedNOQA(_)
|
||||
|
||||
@@ -43,7 +43,9 @@ pub enum CheckCodePrefix {
|
||||
B006,
|
||||
B007,
|
||||
B008,
|
||||
B009,
|
||||
B01,
|
||||
B010,
|
||||
B011,
|
||||
B013,
|
||||
B014,
|
||||
@@ -51,6 +53,7 @@ pub enum CheckCodePrefix {
|
||||
B016,
|
||||
B017,
|
||||
B018,
|
||||
B019,
|
||||
B02,
|
||||
B025,
|
||||
B026,
|
||||
@@ -202,6 +205,10 @@ pub enum CheckCodePrefix {
|
||||
F9,
|
||||
F90,
|
||||
F901,
|
||||
I,
|
||||
I0,
|
||||
I00,
|
||||
I001,
|
||||
M,
|
||||
M0,
|
||||
M00,
|
||||
@@ -238,6 +245,15 @@ pub enum CheckCodePrefix {
|
||||
RUF001,
|
||||
RUF002,
|
||||
RUF003,
|
||||
S,
|
||||
S1,
|
||||
S10,
|
||||
S101,
|
||||
S102,
|
||||
S104,
|
||||
S105,
|
||||
S106,
|
||||
S107,
|
||||
T,
|
||||
T2,
|
||||
T20,
|
||||
@@ -258,6 +274,7 @@ pub enum CheckCodePrefix {
|
||||
U01,
|
||||
U010,
|
||||
U011,
|
||||
U012,
|
||||
W,
|
||||
W2,
|
||||
W29,
|
||||
@@ -265,6 +282,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)]
|
||||
@@ -336,6 +370,8 @@ impl CheckCodePrefix {
|
||||
CheckCode::B006,
|
||||
CheckCode::B007,
|
||||
CheckCode::B008,
|
||||
CheckCode::B009,
|
||||
CheckCode::B010,
|
||||
CheckCode::B011,
|
||||
CheckCode::B013,
|
||||
CheckCode::B014,
|
||||
@@ -343,6 +379,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::B016,
|
||||
CheckCode::B017,
|
||||
CheckCode::B018,
|
||||
CheckCode::B019,
|
||||
CheckCode::B025,
|
||||
CheckCode::B026,
|
||||
],
|
||||
@@ -354,6 +391,8 @@ impl CheckCodePrefix {
|
||||
CheckCode::B006,
|
||||
CheckCode::B007,
|
||||
CheckCode::B008,
|
||||
CheckCode::B009,
|
||||
CheckCode::B010,
|
||||
CheckCode::B011,
|
||||
CheckCode::B013,
|
||||
CheckCode::B014,
|
||||
@@ -361,6 +400,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::B016,
|
||||
CheckCode::B017,
|
||||
CheckCode::B018,
|
||||
CheckCode::B019,
|
||||
CheckCode::B025,
|
||||
CheckCode::B026,
|
||||
],
|
||||
@@ -372,6 +412,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::B006,
|
||||
CheckCode::B007,
|
||||
CheckCode::B008,
|
||||
CheckCode::B009,
|
||||
],
|
||||
CheckCodePrefix::B002 => vec![CheckCode::B002],
|
||||
CheckCodePrefix::B003 => vec![CheckCode::B003],
|
||||
@@ -380,7 +421,9 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::B006 => vec![CheckCode::B006],
|
||||
CheckCodePrefix::B007 => vec![CheckCode::B007],
|
||||
CheckCodePrefix::B008 => vec![CheckCode::B008],
|
||||
CheckCodePrefix::B009 => vec![CheckCode::B009],
|
||||
CheckCodePrefix::B01 => vec![
|
||||
CheckCode::B010,
|
||||
CheckCode::B011,
|
||||
CheckCode::B013,
|
||||
CheckCode::B014,
|
||||
@@ -388,7 +431,9 @@ impl CheckCodePrefix {
|
||||
CheckCode::B016,
|
||||
CheckCode::B017,
|
||||
CheckCode::B018,
|
||||
CheckCode::B019,
|
||||
],
|
||||
CheckCodePrefix::B010 => vec![CheckCode::B010],
|
||||
CheckCodePrefix::B011 => vec![CheckCode::B011],
|
||||
CheckCodePrefix::B013 => vec![CheckCode::B013],
|
||||
CheckCodePrefix::B014 => vec![CheckCode::B014],
|
||||
@@ -396,6 +441,7 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::B016 => vec![CheckCode::B016],
|
||||
CheckCodePrefix::B017 => vec![CheckCode::B017],
|
||||
CheckCodePrefix::B018 => vec![CheckCode::B018],
|
||||
CheckCodePrefix::B019 => vec![CheckCode::B019],
|
||||
CheckCodePrefix::B02 => vec![CheckCode::B025, CheckCode::B026],
|
||||
CheckCodePrefix::B025 => vec![CheckCode::B025],
|
||||
CheckCodePrefix::B026 => vec![CheckCode::B026],
|
||||
@@ -847,6 +893,10 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::F9 => vec![CheckCode::F901],
|
||||
CheckCodePrefix::F90 => vec![CheckCode::F901],
|
||||
CheckCodePrefix::F901 => vec![CheckCode::F901],
|
||||
CheckCodePrefix::I => vec![CheckCode::I001],
|
||||
CheckCodePrefix::I0 => vec![CheckCode::I001],
|
||||
CheckCodePrefix::I00 => vec![CheckCode::I001],
|
||||
CheckCodePrefix::I001 => vec![CheckCode::I001],
|
||||
CheckCodePrefix::M => vec![CheckCode::M001],
|
||||
CheckCodePrefix::M0 => vec![CheckCode::M001],
|
||||
CheckCodePrefix::M00 => vec![CheckCode::M001],
|
||||
@@ -947,6 +997,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],
|
||||
@@ -964,6 +1044,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::U009,
|
||||
CheckCode::U010,
|
||||
CheckCode::U011,
|
||||
CheckCode::U012,
|
||||
],
|
||||
CheckCodePrefix::U0 => vec![
|
||||
CheckCode::U001,
|
||||
@@ -977,6 +1058,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::U009,
|
||||
CheckCode::U010,
|
||||
CheckCode::U011,
|
||||
CheckCode::U012,
|
||||
],
|
||||
CheckCodePrefix::U00 => vec![
|
||||
CheckCode::U001,
|
||||
@@ -998,9 +1080,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],
|
||||
@@ -1008,6 +1091,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],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1051,7 +1172,9 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::B006 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B007 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B008 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B009 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B01 => PrefixSpecificity::Tens,
|
||||
CheckCodePrefix::B010 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B011 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B013 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B014 => PrefixSpecificity::Explicit,
|
||||
@@ -1059,6 +1182,7 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::B016 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B017 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B018 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B019 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B02 => PrefixSpecificity::Tens,
|
||||
CheckCodePrefix::B025 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B026 => PrefixSpecificity::Explicit,
|
||||
@@ -1210,6 +1334,19 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::F9 => PrefixSpecificity::Hundreds,
|
||||
CheckCodePrefix::F90 => PrefixSpecificity::Tens,
|
||||
CheckCodePrefix::F901 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::I => PrefixSpecificity::Category,
|
||||
CheckCodePrefix::I0 => PrefixSpecificity::Hundreds,
|
||||
CheckCodePrefix::I00 => PrefixSpecificity::Tens,
|
||||
CheckCodePrefix::I001 => 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::M => PrefixSpecificity::Category,
|
||||
CheckCodePrefix::M0 => PrefixSpecificity::Hundreds,
|
||||
CheckCodePrefix::M00 => PrefixSpecificity::Tens,
|
||||
@@ -1266,6 +1403,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,
|
||||
@@ -1273,6 +1411,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
206
src/directives.rs
Normal file
206
src/directives.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
//! Extract `# noqa` and `# isort: skip` directives from tokenized source.
|
||||
|
||||
use bitflags::bitflags;
|
||||
use nohash_hasher::{IntMap, IntSet};
|
||||
use rustpython_ast::Location;
|
||||
use rustpython_parser::lexer::{LexResult, Tok};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::LintSource;
|
||||
use crate::{Settings, SourceCodeLocator};
|
||||
|
||||
bitflags! {
|
||||
pub struct Flags: u32 {
|
||||
const NOQA = 0b00000001;
|
||||
const ISORT = 0b00000010;
|
||||
}
|
||||
}
|
||||
|
||||
impl Flags {
|
||||
pub fn from_settings(settings: &Settings) -> Self {
|
||||
if settings
|
||||
.enabled
|
||||
.iter()
|
||||
.any(|check_code| matches!(check_code.lint_source(), LintSource::Imports))
|
||||
{
|
||||
Flags::NOQA | Flags::ISORT
|
||||
} else {
|
||||
Flags::NOQA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Directives {
|
||||
pub noqa_line_for: IntMap<usize, usize>,
|
||||
pub isort_exclusions: IntSet<usize>,
|
||||
}
|
||||
|
||||
pub fn extract_directives(
|
||||
lxr: &[LexResult],
|
||||
locator: &SourceCodeLocator,
|
||||
flags: &Flags,
|
||||
) -> Directives {
|
||||
Directives {
|
||||
noqa_line_for: if flags.contains(Flags::NOQA) {
|
||||
extract_noqa_line_for(lxr)
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
isort_exclusions: if flags.contains(Flags::ISORT) {
|
||||
extract_isort_exclusions(lxr, locator)
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a mapping from logical line to noqa line.
|
||||
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
|
||||
let mut noqa_line_for: IntMap<usize, usize> = IntMap::default();
|
||||
for (start, tok, end) in lxr.iter().flatten() {
|
||||
if matches!(tok, Tok::EndOfFile) {
|
||||
break;
|
||||
}
|
||||
// For multi-line strings, we expect `noqa` directives on the last line of the
|
||||
// string.
|
||||
if matches!(tok, Tok::String { .. }) && end.row() > start.row() {
|
||||
for i in start.row()..end.row() {
|
||||
noqa_line_for.insert(i, end.row());
|
||||
}
|
||||
}
|
||||
}
|
||||
noqa_line_for
|
||||
}
|
||||
|
||||
/// Extract a set of lines over which to disable isort.
|
||||
pub fn extract_isort_exclusions(lxr: &[LexResult], locator: &SourceCodeLocator) -> IntSet<usize> {
|
||||
let mut exclusions: IntSet<usize> = IntSet::default();
|
||||
let mut off: Option<&Location> = None;
|
||||
for (start, tok, end) in lxr.iter().flatten() {
|
||||
// TODO(charlie): Modify RustPython to include the comment text in the token.
|
||||
if matches!(tok, Tok::Comment) {
|
||||
let comment_text = locator.slice_source_code_range(&Range {
|
||||
location: *start,
|
||||
end_location: *end,
|
||||
});
|
||||
if off.is_some() {
|
||||
if comment_text == "# isort: on" {
|
||||
if let Some(start) = off {
|
||||
for row in start.row() + 1..=end.row() {
|
||||
exclusions.insert(row);
|
||||
}
|
||||
}
|
||||
off = None;
|
||||
}
|
||||
} else {
|
||||
if comment_text.contains("isort: skip") || comment_text.contains("isort:skip") {
|
||||
exclusions.insert(start.row());
|
||||
} else if comment_text == "# isort: off" {
|
||||
off = Some(start);
|
||||
}
|
||||
}
|
||||
} else if matches!(tok, Tok::EndOfFile) {
|
||||
if let Some(start) = off {
|
||||
for row in start.row() + 1..=end.row() {
|
||||
exclusions.insert(row);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
exclusions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use nohash_hasher::IntMap;
|
||||
use rustpython_parser::lexer;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
|
||||
use crate::directives::extract_noqa_line_for;
|
||||
|
||||
#[test]
|
||||
fn extraction() -> Result<()> {
|
||||
let empty: IntMap<usize, usize> = Default::default();
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = 2
|
||||
z = x + 1",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"
|
||||
x = 1
|
||||
y = 2
|
||||
z = x + 1",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = 2
|
||||
z = x + 1
|
||||
",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
|
||||
y = 2
|
||||
z = x + 1
|
||||
",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), empty);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = '''abc
|
||||
def
|
||||
ghi
|
||||
'''
|
||||
y = 2
|
||||
z = x + 1",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(1, 4), (2, 4), (3, 4)])
|
||||
);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = '''abc
|
||||
def
|
||||
ghi
|
||||
'''
|
||||
z = 2",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
|
||||
);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(
|
||||
"x = 1
|
||||
y = '''abc
|
||||
def
|
||||
ghi
|
||||
'''",
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use rustpython_ast::{Expr, Location};
|
||||
use rustpython_ast::{Located, Location};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
@@ -24,9 +24,9 @@ pub fn leading_space(line: &str) -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Extract the leading indentation from a docstring.
|
||||
pub fn indentation<'a>(checker: &'a Checker, docstring: &Expr) -> String {
|
||||
let range = Range::from_located(docstring);
|
||||
/// Extract the leading indentation from a line.
|
||||
pub fn indentation<'a, T>(checker: &'a Checker, located: &Located<T>) -> String {
|
||||
let range = Range::from_located(located);
|
||||
checker
|
||||
.locator
|
||||
.slice_source_code_range(&Range {
|
||||
|
||||
1
src/flake8_2020/mod.rs
Normal file
1
src/flake8_2020/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod plugins;
|
||||
192
src/flake8_2020/plugins.rs
Normal file
192
src/flake8_2020/plugins.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use num_bigint::BigInt;
|
||||
use rustpython_ast::{Cmpop, Constant, Expr, ExprKind, Located};
|
||||
|
||||
use crate::ast::helpers::match_name_or_attr_from_module;
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
|
||||
fn is_sys(checker: &Checker, expr: &Expr, target: &str) -> bool {
|
||||
match_name_or_attr_from_module(expr, target, "sys", checker.from_imports.get("sys"))
|
||||
}
|
||||
|
||||
/// 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_name_or_attr_from_module(expr, "PY3", "six", checker.from_imports.get("six"))
|
||||
&& checker.settings.enabled.contains(&CheckCode::YTT202)
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SixPY3Referenced,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -6,32 +6,15 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
use crate::linter::tokenize;
|
||||
use crate::{flake8_annotations, fs, linter, noqa, Settings, SourceCodeLocator};
|
||||
|
||||
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
|
||||
let contents = fs::read_file(path)?;
|
||||
let tokens: Vec<LexResult> = tokenize(&contents);
|
||||
let locator = SourceCodeLocator::new(&contents);
|
||||
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
|
||||
linter::check_path(
|
||||
path,
|
||||
&contents,
|
||||
tokens,
|
||||
&locator,
|
||||
&noqa_line_for,
|
||||
settings,
|
||||
autofix,
|
||||
)
|
||||
}
|
||||
use crate::checks::CheckCode;
|
||||
use crate::linter::test_path;
|
||||
use crate::{flake8_annotations, Settings};
|
||||
|
||||
#[test]
|
||||
fn defaults() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/annotation_presence.py"),
|
||||
&Settings {
|
||||
..Settings::for_rules(vec![
|
||||
@@ -57,7 +40,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn suppress_dummy_args() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/suppress_dummy_args.py"),
|
||||
&Settings {
|
||||
flake8_annotations: flake8_annotations::settings::Settings {
|
||||
@@ -83,7 +66,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mypy_init_return() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/mypy_init_return.py"),
|
||||
&Settings {
|
||||
flake8_annotations: flake8_annotations::settings::Settings {
|
||||
@@ -109,7 +92,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn suppress_none_returning() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/suppress_none_returning.py"),
|
||||
&Settings {
|
||||
flake8_annotations: flake8_annotations::settings::Settings {
|
||||
@@ -135,7 +118,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn allow_star_arg_any() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/allow_star_arg_any.py"),
|
||||
&Settings {
|
||||
flake8_annotations: flake8_annotations::settings::Settings {
|
||||
|
||||
22
src/flake8_bandit/helpers.rs
Normal file
22
src/flake8_bandit/helpers.rs
Normal 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
2
src/flake8_bandit/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod helpers;
|
||||
pub mod plugins;
|
||||
9
src/flake8_bandit/plugins/assert_used.rs
Normal file
9
src/flake8_bandit/plugins/assert_used.rs
Normal 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))
|
||||
}
|
||||
14
src/flake8_bandit/plugins/exec_used.rs
Normal file
14
src/flake8_bandit/plugins/exec_used.rs
Normal 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
|
||||
}
|
||||
11
src/flake8_bandit/plugins/hardcoded_bind_all_interfaces.rs
Normal file
11
src/flake8_bandit/plugins/hardcoded_bind_all_interfaces.rs
Normal 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
|
||||
}
|
||||
}
|
||||
51
src/flake8_bandit/plugins/hardcoded_password_default.rs
Normal file
51
src/flake8_bandit/plugins/hardcoded_password_default.rs
Normal 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
|
||||
}
|
||||
25
src/flake8_bandit/plugins/hardcoded_password_func_arg.rs
Normal file
25
src/flake8_bandit/plugins/hardcoded_password_func_arg.rs
Normal 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()
|
||||
}
|
||||
58
src/flake8_bandit/plugins/hardcoded_password_string.rs
Normal file
58
src/flake8_bandit/plugins/hardcoded_password_string.rs
Normal 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
|
||||
}
|
||||
15
src/flake8_bandit/plugins/mod.rs
Normal file
15
src/flake8_bandit/plugins/mod.rs
Normal 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;
|
||||
5
src/flake8_bugbear/constants.rs
Normal file
5
src/flake8_bugbear/constants.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
pub static IDENTIFIER_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap());
|
||||
@@ -1 +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(())
|
||||
}
|
||||
}
|
||||
|
||||
49
src/flake8_bugbear/plugins/cached_instance_method.rs
Normal file
49
src/flake8_bugbear/plugins/cached_instance_method.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use rustpython_ast::{Expr, ExprKind};
|
||||
|
||||
use crate::ast::helpers::{compose_call_path, match_name_or_attr_from_module};
|
||||
use crate::ast::types::{Range, ScopeKind};
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
fn is_cache_func(checker: &Checker, expr: &Expr) -> bool {
|
||||
match_name_or_attr_from_module(
|
||||
expr,
|
||||
"lru_cache",
|
||||
"functools",
|
||||
checker.from_imports.get("functools"),
|
||||
) || match_name_or_attr_from_module(
|
||||
expr,
|
||||
"cache",
|
||||
"functools",
|
||||
checker.from_imports.get("functools"),
|
||||
)
|
||||
}
|
||||
|
||||
/// 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 Some(decorator_path) = compose_call_path(decorator) {
|
||||
if decorator_path == "classmethod" || decorator_path == "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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use fnv::{FnvHashMap, FnvHashSet};
|
||||
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
|
||||
|
||||
use crate::ast::helpers::compose_call_path;
|
||||
@@ -8,37 +9,69 @@ 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] = [
|
||||
const IMMUTABLE_FUNCS: [&str; 7] = [
|
||||
"tuple",
|
||||
"frozenset",
|
||||
"operator.attrgetter",
|
||||
"operator.itemgetter",
|
||||
"operator.methodcaller",
|
||||
"attrgetter",
|
||||
"itemgetter",
|
||||
"methodcaller",
|
||||
"types.MappingProxyType",
|
||||
"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],
|
||||
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
|
||||
) -> bool {
|
||||
compose_call_path(expr).map_or_else(
|
||||
|| false,
|
||||
|call_path| {
|
||||
// It matches the call path exactly (`operator.methodcaller`).
|
||||
for target in IMMUTABLE_FUNCS.iter().chain(extend_immutable_calls) {
|
||||
if &call_path == target {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// It matches the member name, and was imported from that module (`methodcaller`
|
||||
// following `from operator import methodcaller`).
|
||||
if !call_path.contains('.') {
|
||||
for target in IMMUTABLE_FUNCS.iter().chain(extend_immutable_calls) {
|
||||
let mut splitter = target.rsplit('.');
|
||||
if let (Some(member), Some(module)) = (splitter.next(), splitter.next()) {
|
||||
if call_path == member
|
||||
&& from_imports
|
||||
.get(module)
|
||||
.map(|module| module.contains(member))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
struct ArgumentDefaultVisitor {
|
||||
struct ArgumentDefaultVisitor<'a> {
|
||||
checks: Vec<(CheckKind, Range)>,
|
||||
extend_immutable_calls: &'a [&'a str],
|
||||
from_imports: &'a FnvHashMap<&'a str, FnvHashSet<&'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)
|
||||
&& !is_immutable_func(func, self.extend_immutable_calls, self.from_imports)
|
||||
&& !is_nan_or_infinity(func, args)
|
||||
{
|
||||
self.checks.push((
|
||||
@@ -82,7 +115,18 @@ 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![] };
|
||||
let extend_immutable_cells: Vec<&str> = checker
|
||||
.settings
|
||||
.flake8_bugbear
|
||||
.extend_immutable_calls
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect();
|
||||
let mut visitor = ArgumentDefaultVisitor {
|
||||
checks: vec![],
|
||||
extend_immutable_calls: &extend_immutable_cells,
|
||||
from_imports: &checker.from_imports,
|
||||
};
|
||||
for expr in arguments
|
||||
.defaults
|
||||
.iter()
|
||||
|
||||
53
src/flake8_bugbear/plugins/getattr_with_constant.rs
Normal file
53
src/flake8_bugbear/plugins/getattr_with_constant.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::code_gen::SourceGenerator;
|
||||
use crate::flake8_bugbear::constants::IDENTIFIER_REGEX;
|
||||
use crate::python::keyword::KWLIST;
|
||||
|
||||
fn attribute(value: &Expr, attr: &str) -> Expr {
|
||||
Expr::new(
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
ExprKind::Attribute {
|
||||
value: Box::new(value.clone()),
|
||||
attr: attr.to_string(),
|
||||
ctx: ExprContext::Load,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "getattr" {
|
||||
if let [obj, arg] = args {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
} = &arg.node
|
||||
{
|
||||
if IDENTIFIER_REGEX.is_match(value) && !KWLIST.contains(&value.as_str()) {
|
||||
let mut check =
|
||||
Check::new(CheckKind::GetAttrWithConstant, Range::from_located(expr));
|
||||
if checker.patch() {
|
||||
let mut generator = SourceGenerator::new();
|
||||
if let Ok(()) = generator.unparse_expr(&attribute(obj, value), 0) {
|
||||
if let Ok(content) = generator.generate() {
|
||||
check.amend(Fix::replacement(
|
||||
content,
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
checker.add_check(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
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 function_call_argument_default::function_call_argument_default;
|
||||
pub use getattr_with_constant::getattr_with_constant;
|
||||
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;
|
||||
pub use star_arg_unpacking_after_keyword_arg::star_arg_unpacking_after_keyword_arg;
|
||||
pub use strip_with_multi_characters::strip_with_multi_characters;
|
||||
pub use unary_prefix_increment::unary_prefix_increment;
|
||||
@@ -17,11 +20,14 @@ pub use useless_expression::useless_expression;
|
||||
mod assert_false;
|
||||
mod assert_raises_exception;
|
||||
mod assignment_to_os_environ;
|
||||
mod cached_instance_method;
|
||||
mod cannot_raise_literal;
|
||||
mod duplicate_exceptions;
|
||||
mod function_call_argument_default;
|
||||
mod getattr_with_constant;
|
||||
mod mutable_argument_default;
|
||||
mod redundant_tuple_in_exception_handler;
|
||||
mod setattr_with_constant;
|
||||
mod star_arg_unpacking_after_keyword_arg;
|
||||
mod strip_with_multi_characters;
|
||||
mod unary_prefix_increment;
|
||||
|
||||
@@ -1,32 +1,53 @@
|
||||
use fnv::{FnvHashMap, FnvHashSet};
|
||||
use rustpython_ast::{Arguments, Expr, ExprKind};
|
||||
|
||||
use crate::ast::helpers::compose_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; 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>>) -> bool {
|
||||
compose_call_path(expr).map_or_else(
|
||||
|| false,
|
||||
|call_path| {
|
||||
// It matches the call path exactly (`collections.Counter`).
|
||||
for target in MUTABLE_FUNCS {
|
||||
if call_path == target {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// It matches the member name, and was imported from that module (`Counter`
|
||||
// following `from collections import Counter`).
|
||||
if !call_path.contains('.') {
|
||||
for target in MUTABLE_FUNCS {
|
||||
let mut splitter = target.rsplit('.');
|
||||
if let (Some(member), Some(module)) = (splitter.next(), splitter.next()) {
|
||||
if call_path == member
|
||||
&& from_imports
|
||||
.get(module)
|
||||
.map(|module| module.contains(member))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// B006
|
||||
@@ -49,7 +70,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.add_check(Check::new(
|
||||
CheckKind::MutableArgumentDefault,
|
||||
Range::from_located(expr),
|
||||
|
||||
29
src/flake8_bugbear/plugins/setattr_with_constant.rs
Normal file
29
src/flake8_bugbear/plugins/setattr_with_constant.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use rustpython_ast::{Constant, Expr, ExprKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::flake8_bugbear::constants::IDENTIFIER_REGEX;
|
||||
use crate::python::keyword::KWLIST;
|
||||
|
||||
/// B010
|
||||
pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if id == "setattr" {
|
||||
if let [_, arg, _] = args {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
} = &arg.node
|
||||
{
|
||||
if IDENTIFIER_REGEX.is_match(value) && !KWLIST.contains(&value.as_str()) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SetAttrWithConstant,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<'_> {
|
||||
|
||||
22
src/flake8_bugbear/settings.rs
Normal file
22
src/flake8_bugbear/settings.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: src/flake8_bugbear/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: FunctionCallArgumentDefault
|
||||
location:
|
||||
row: 19
|
||||
column: 50
|
||||
end_location:
|
||||
row: 19
|
||||
column: 63
|
||||
fix: ~
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,30 +6,13 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
use crate::checks::CheckCode;
|
||||
use crate::flake8_quotes::settings::Quote;
|
||||
use crate::linter::tokenize;
|
||||
use crate::{flake8_quotes, fs, linter, noqa, Settings, SourceCodeLocator};
|
||||
|
||||
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
|
||||
let contents = fs::read_file(path)?;
|
||||
let tokens: Vec<LexResult> = tokenize(&contents);
|
||||
let locator = SourceCodeLocator::new(&contents);
|
||||
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
|
||||
linter::check_path(
|
||||
path,
|
||||
&contents,
|
||||
tokens,
|
||||
&locator,
|
||||
&noqa_line_for,
|
||||
settings,
|
||||
autofix,
|
||||
)
|
||||
}
|
||||
use crate::linter::test_path;
|
||||
use crate::{flake8_quotes, Settings};
|
||||
|
||||
#[test_case(Path::new("doubles.py"))]
|
||||
#[test_case(Path::new("doubles_escaped.py"))]
|
||||
@@ -38,7 +21,7 @@ mod tests {
|
||||
#[test_case(Path::new("doubles_wrapped.py"))]
|
||||
fn doubles(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("doubles_{}", path.to_string_lossy());
|
||||
let mut checks = check_path(
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_quotes")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
@@ -70,7 +53,7 @@ mod tests {
|
||||
#[test_case(Path::new("singles_wrapped.py"))]
|
||||
fn singles(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("singles_{}", path.to_string_lossy());
|
||||
let mut checks = check_path(
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_quotes")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
@@ -107,7 +90,7 @@ mod tests {
|
||||
#[test_case(Path::new("docstring_singles_function.py"))]
|
||||
fn double_docstring(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("double_docstring_{}", path.to_string_lossy());
|
||||
let mut checks = check_path(
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_quotes")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
@@ -144,7 +127,7 @@ mod tests {
|
||||
#[test_case(Path::new("docstring_singles_function.py"))]
|
||||
fn single_docstring(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("single_docstring_{}", path.to_string_lossy());
|
||||
let mut checks = check_path(
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_quotes")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
|
||||
67
src/isort/categorize.rs
Normal file
67
src/isort/categorize.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::collections::{BTreeMap, 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)]
|
||||
pub enum ImportType {
|
||||
Future,
|
||||
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 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 KNOWN_STANDARD_LIBRARY.contains(module_base) {
|
||||
ImportType::StandardLibrary
|
||||
} else if find_local(src, module_base) {
|
||||
ImportType::FirstParty
|
||||
} else {
|
||||
ImportType::ThirdParty
|
||||
}
|
||||
}
|
||||
|
||||
static STATIC_CLASSIFICATIONS: Lazy<BTreeMap<&'static str, ImportType>> = Lazy::new(|| {
|
||||
BTreeMap::from([
|
||||
("__future__", ImportType::Future),
|
||||
// Relative imports (e.g., `from . import module`).
|
||||
("", ImportType::FirstParty),
|
||||
])
|
||||
});
|
||||
|
||||
fn find_local(paths: &[PathBuf], base: &str) -> bool {
|
||||
for path in paths {
|
||||
if let Ok(metadata) = fs::metadata(path.join(base)) {
|
||||
if metadata.is_dir() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Ok(metadata) = fs::metadata(path.join(format!("{base}.py"))) {
|
||||
if metadata.is_file() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
272
src/isort/mod.rs
Normal file
272
src/isort/mod.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use itertools::Itertools;
|
||||
use ropey::RopeBuilder;
|
||||
use rustpython_ast::{Stmt, StmtKind};
|
||||
|
||||
use crate::isort::categorize::{categorize, ImportType};
|
||||
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;
|
||||
|
||||
// Hard-code four-space indentation for the imports themselves, to match Black.
|
||||
const INDENT: &str = " ";
|
||||
|
||||
fn normalize_imports<'a>(imports: &'a [&'a Stmt]) -> ImportBlock<'a> {
|
||||
let mut block: ImportBlock = Default::default();
|
||||
for import in imports {
|
||||
match &import.node {
|
||||
StmtKind::Import { names } => {
|
||||
for name in names {
|
||||
block.import.insert(AliasData {
|
||||
name: &name.node.name,
|
||||
asname: &name.node.asname,
|
||||
});
|
||||
}
|
||||
}
|
||||
StmtKind::ImportFrom {
|
||||
module,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
|
||||
}
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
fn categorize_imports<'a>(
|
||||
block: ImportBlock<'a>,
|
||||
src: &[PathBuf],
|
||||
known_first_party: &BTreeSet<String>,
|
||||
known_third_party: &BTreeSet<String>,
|
||||
extra_standard_library: &BTreeSet<String>,
|
||||
) -> BTreeMap<ImportType, ImportBlock<'a>> {
|
||||
let mut block_by_type: BTreeMap<ImportType, ImportBlock> = Default::default();
|
||||
// Categorize `StmtKind::Import`.
|
||||
for alias in block.import {
|
||||
let import_type = categorize(
|
||||
&alias.module_base(),
|
||||
&None,
|
||||
src,
|
||||
known_first_party,
|
||||
known_third_party,
|
||||
extra_standard_library,
|
||||
);
|
||||
block_by_type
|
||||
.entry(import_type)
|
||||
.or_default()
|
||||
.import
|
||||
.insert(alias);
|
||||
}
|
||||
// Categorize `StmtKind::ImportFrom`.
|
||||
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,
|
||||
extra_standard_library,
|
||||
);
|
||||
block_by_type
|
||||
.entry(classification)
|
||||
.or_default()
|
||||
.import_from
|
||||
.insert(import_from, aliases);
|
||||
}
|
||||
block_by_type
|
||||
}
|
||||
|
||||
fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
|
||||
let mut ordered: OrderedImportBlock = Default::default();
|
||||
// Sort `StmtKind::Import`.
|
||||
for import in block
|
||||
.import
|
||||
.into_iter()
|
||||
.sorted_by_cached_key(|alias| module_key(alias.name))
|
||||
{
|
||||
ordered.import.push(import);
|
||||
}
|
||||
// Sort `StmtKind::ImportFrom`.
|
||||
for (import_from, aliases) in
|
||||
block
|
||||
.import_from
|
||||
.into_iter()
|
||||
.sorted_by_cached_key(|(import_from, _)| {
|
||||
import_from.module.as_ref().map(|module| module_key(module))
|
||||
})
|
||||
{
|
||||
ordered.import_from.push((
|
||||
import_from,
|
||||
aliases
|
||||
.into_iter()
|
||||
.sorted_by_cached_key(|alias| member_key(alias.name))
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
ordered
|
||||
}
|
||||
|
||||
pub fn format_imports(
|
||||
block: Vec<&Stmt>,
|
||||
line_length: &usize,
|
||||
src: &[PathBuf],
|
||||
known_first_party: &BTreeSet<String>,
|
||||
known_third_party: &BTreeSet<String>,
|
||||
extra_standard_library: &BTreeSet<String>,
|
||||
) -> String {
|
||||
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
|
||||
let block = normalize_imports(&block);
|
||||
|
||||
// Categorize by type (e.g., first-party vs. third-party).
|
||||
let block_by_type = categorize_imports(
|
||||
block,
|
||||
src,
|
||||
known_first_party,
|
||||
known_third_party,
|
||||
extra_standard_library,
|
||||
);
|
||||
|
||||
// Generate replacement source code.
|
||||
let mut output = RopeBuilder::new();
|
||||
let mut first_block = true;
|
||||
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 {
|
||||
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();
|
||||
|
||||
// Can we fit the import on a single line?
|
||||
let expected_len: usize =
|
||||
// `from base import `
|
||||
prelude.len()
|
||||
// `member( as alias)?`
|
||||
+ members.iter().map(|part| part.len()).sum::<usize>()
|
||||
// `, `
|
||||
+ 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);
|
||||
}
|
||||
// `\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(")");
|
||||
output.append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
output.finish().to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::CheckCode;
|
||||
use crate::linter::test_path;
|
||||
use crate::Settings;
|
||||
|
||||
#[test_case(Path::new("combine_import_froms.py"))]
|
||||
#[test_case(Path::new("deduplicate_imports.py"))]
|
||||
#[test_case(Path::new("fit_line_length.py"))]
|
||||
#[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("trailing_suffix.py"))]
|
||||
fn isort(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}", path.to_string_lossy());
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/isort")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
&Settings {
|
||||
src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()],
|
||||
..Settings::for_rule(CheckCode::I001)
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
116
src/isort/plugins.rs
Normal file
116
src/isort/plugins.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use rustpython_ast::{Location, Stmt};
|
||||
use textwrap::{dedent, indent};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::{fixer, Fix};
|
||||
use crate::checks::CheckKind;
|
||||
use crate::docstrings::helpers::leading_space;
|
||||
use crate::isort::format_imports;
|
||||
use crate::{Check, Settings, SourceCodeLocator};
|
||||
|
||||
fn extract_range(body: &[&Stmt]) -> Range {
|
||||
let location = body.first().unwrap().location;
|
||||
let end_location = body.last().unwrap().end_location.unwrap();
|
||||
Range {
|
||||
location,
|
||||
end_location,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_indentation(body: &[&Stmt], locator: &SourceCodeLocator) -> String {
|
||||
let location = body.first().unwrap().location;
|
||||
let range = Range {
|
||||
location: Location::new(location.row(), 0),
|
||||
end_location: location,
|
||||
};
|
||||
let existing = locator.slice_source_code_range(&range);
|
||||
leading_space(&existing)
|
||||
}
|
||||
|
||||
fn match_leading_content(body: &[&Stmt], locator: &SourceCodeLocator) -> bool {
|
||||
let location = body.first().unwrap().location;
|
||||
let range = Range {
|
||||
location: Location::new(location.row(), 0),
|
||||
end_location: location,
|
||||
};
|
||||
let prefix = locator.slice_source_code_range(&range);
|
||||
prefix.chars().any(|char| !char.is_whitespace())
|
||||
}
|
||||
|
||||
fn match_trailing_content(body: &[&Stmt], locator: &SourceCodeLocator) -> bool {
|
||||
let end_location = body.last().unwrap().end_location.unwrap();
|
||||
let range = Range {
|
||||
location: end_location,
|
||||
end_location: Location::new(end_location.row() + 1, 0),
|
||||
};
|
||||
let suffix = locator.slice_source_code_range(&range);
|
||||
suffix.chars().any(|char| !char.is_whitespace())
|
||||
}
|
||||
|
||||
/// I001
|
||||
pub fn check_imports(
|
||||
body: Vec<&Stmt>,
|
||||
locator: &SourceCodeLocator,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Option<Check> {
|
||||
let range = extract_range(&body);
|
||||
let indentation = extract_indentation(&body, locator);
|
||||
|
||||
// Special-cases: there's leading or trailing content in the import block.
|
||||
let has_leading_content = match_leading_content(&body, locator);
|
||||
let has_trailing_content = match_trailing_content(&body, locator);
|
||||
|
||||
// Generate the sorted import block.
|
||||
let expected = format_imports(
|
||||
body,
|
||||
&(settings.line_length - indentation.len()),
|
||||
&settings.src,
|
||||
&settings.isort.known_first_party,
|
||||
&settings.isort.known_third_party,
|
||||
&settings.isort.extra_standard_library,
|
||||
);
|
||||
|
||||
if has_leading_content || has_trailing_content {
|
||||
let mut check = Check::new(CheckKind::UnsortedImports, range);
|
||||
if autofix.patch() {
|
||||
let mut content = String::new();
|
||||
if has_leading_content {
|
||||
content.push('\n');
|
||||
}
|
||||
content.push_str(&indent(&expected, &indentation));
|
||||
check.amend(Fix::replacement(
|
||||
content,
|
||||
// Preserve leading prefix (but put the imports on a new line).
|
||||
if has_leading_content {
|
||||
range.location
|
||||
} else {
|
||||
Location::new(range.location.row(), 0)
|
||||
},
|
||||
// TODO(charlie): Preserve trailing suffixes. Right now, we strip them.
|
||||
Location::new(range.end_location.row() + 1, 0),
|
||||
));
|
||||
}
|
||||
Some(check)
|
||||
} else {
|
||||
// Expand the span the entire range, including leading and trailing space.
|
||||
let range = Range {
|
||||
location: Location::new(range.location.row(), 0),
|
||||
end_location: Location::new(range.end_location.row() + 1, 0),
|
||||
};
|
||||
let actual = dedent(&locator.slice_source_code_range(&range));
|
||||
if actual != expected {
|
||||
let mut check = Check::new(CheckKind::UnsortedImports, range);
|
||||
if autofix.patch() {
|
||||
check.amend(Fix::replacement(
|
||||
indent(&expected, &indentation),
|
||||
range.location,
|
||||
range.end_location,
|
||||
));
|
||||
}
|
||||
Some(check)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/isort/settings.rs
Normal file
32
src/isort/settings.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Settings for the `isort` plugin.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
pub struct Options {
|
||||
pub known_first_party: Option<Vec<String>>,
|
||||
pub known_third_party: Option<Vec<String>>,
|
||||
pub extra_standard_library: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, Default)]
|
||||
pub struct Settings {
|
||||
pub known_first_party: BTreeSet<String>,
|
||||
pub known_third_party: BTreeSet<String>,
|
||||
pub extra_standard_library: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn from_options(options: Options) -> Self {
|
||||
Self {
|
||||
known_first_party: BTreeSet::from_iter(options.known_first_party.unwrap_or_default()),
|
||||
known_third_party: BTreeSet::from_iter(options.known_third_party.unwrap_or_default()),
|
||||
extra_standard_library: BTreeSet::from_iter(
|
||||
options.extra_standard_library.unwrap_or_default(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: src/isort/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: UnsortedImports
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 6
|
||||
column: 0
|
||||
fix:
|
||||
patch:
|
||||
content: "from collections import (\n AsyncIterable,\n Awaitable,\n ChainMap,\n Collection,\n MutableMapping,\n MutableSequence,\n)\n"
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 6
|
||||
column: 0
|
||||
applied: false
|
||||
|
||||
@@ -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 os as os1\nimport os as os2\n"
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 5
|
||||
column: 0
|
||||
applied: false
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: src/isort/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: UnsortedImports
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 3
|
||||
column: 0
|
||||
fix:
|
||||
patch:
|
||||
content: "import os\nfrom collections import Collection\n"
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 3
|
||||
column: 0
|
||||
applied: false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user