Compare commits

..

31 Commits

Author SHA1 Message Date
Charlie Marsh
b8f517c70e Bump version to 0.0.45 2022-09-22 14:11:09 -04:00
Charlie Marsh
9f601c2abd Document noqa workflows 2022-09-22 14:10:02 -04:00
Charlie Marsh
c0ce0b0c48 Enable automatic noqa insertion (#256) 2022-09-22 13:59:06 -04:00
Charlie Marsh
e5b16973a9 Enable autofix for M001 (#255) 2022-09-22 13:21:03 -04:00
Charlie Marsh
de9ceb2fe1 Only enforce multi-line noqa directives for strings (#258) 2022-09-22 13:09:02 -04:00
Charlie Marsh
38b19b78b7 Enable noqa directives on logical lines (#257) 2022-09-22 12:56:15 -04:00
Charlie Marsh
7043e15b57 Move noqa to a separate module 2022-09-22 09:04:54 -04:00
Charlie Marsh
9594079235 Add --extend-select and --extend-ignore (#254) 2022-09-21 19:56:43 -04:00
Charlie Marsh
732f208e47 Add a lint rule to enforce noqa validity (#253) 2022-09-21 19:56:38 -04:00
Charlie Marsh
32e62d9209 Use specific version tags 2022-09-21 15:11:53 -04:00
Charlie Marsh
d9e4b0cdc1 Implement --show-settings and --show-files (#246) 2022-09-21 15:08:50 -04:00
Charlie Marsh
36fcfad56a Remove empty comment 2022-09-21 13:44:49 -04:00
Charlie Marsh
65d29d9734 Adjust line numbers when reporting rules in f-strings (#244) 2022-09-21 13:42:58 -04:00
Charlie Marsh
1e171ce0e8 Bump version to 0.0.44 2022-09-21 12:25:14 -04:00
Charlie Marsh
2bdc500c61 Re-run cargo insta 2022-09-21 12:24:46 -04:00
Charlie Marsh
f453e429b6 Add a note on parity 2022-09-21 12:24:04 -04:00
Charlie Marsh
73874f4788 Remove proof-of-concept caveat 2022-09-21 12:18:01 -04:00
Charlie Marsh
8846dcdf6a Update README 2022-09-21 12:17:41 -04:00
Charlie Marsh
d827e6e36a Implement F405 (#243) 2022-09-21 12:13:40 -04:00
Harutaka Kawamura
71d9b2ac5f Implement F402 (#221) 2022-09-21 11:12:55 -04:00
Anders Kaseorg
401b53cc45 Handle filesystem errors more consistently (#240) 2022-09-20 23:22:01 -04:00
Anders Kaseorg
aa9c1e255c Simplify check_path type (#239) 2022-09-20 21:11:42 -04:00
Anders Kaseorg
f7fc702b2c Include specified files, even if they lack a .py[i] extension (#238) 2022-09-20 20:53:52 -04:00
Anders Kaseorg
50ca0d7d0a Correctly display the location of parse errors (#237) 2022-09-20 20:53:22 -04:00
Anders Kaseorg
65e0284698 Suppress “Found 0 error(s)” message (#236) 2022-09-20 19:32:39 -04:00
Charlie Marsh
e4f571ea61 Bump version to 0.0.43 2022-09-20 12:26:49 -04:00
Charlie Marsh
4ed88dd245 Follow-up fixes to path absolution (#235) 2022-09-20 12:26:32 -04:00
Charlie Marsh
09b926fd59 Optimize imports 2022-09-20 09:10:39 -04:00
Charlie Marsh
a4869e4974 Update benchmark in README 2022-09-20 07:06:12 -06:00
Charlie Marsh
f53c4fc221 Bump version to 0.0.42 2022-09-19 21:14:17 -06:00
Charlie Marsh
3892a49a97 Bump version to 0.0.41 2022-09-19 21:09:33 -06:00
27 changed files with 1491 additions and 622 deletions

8
Cargo.lock generated
View File

@@ -1801,7 +1801,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.40"
version = "0.0.45"
dependencies = [
"anyhow",
"bincode",
@@ -1846,7 +1846,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=966a80597d626a9a47eaec78471164422d341453#966a80597d626a9a47eaec78471164422d341453"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -1855,7 +1855,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=966a80597d626a9a47eaec78471164422d341453#966a80597d626a9a47eaec78471164422d341453"
dependencies = [
"bincode",
"bitflags",
@@ -1872,7 +1872,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=966a80597d626a9a47eaec78471164422d341453#966a80597d626a9a47eaec78471164422d341453"
dependencies = [
"ahash",
"anyhow",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.40"
version = "0.0.45"
edition = "2021"
[lib]
@@ -18,15 +18,15 @@ common-path = { version = "1.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.17" }
glob = "0.3.0"
itertools = "0.10.3"
glob = { version = "0.3.0" }
itertools = { version = "0.10.3" }
log = { version = "0.4.17" }
notify = { version = "4.0.17" }
once_cell = { version = "1.13.1" }
path-absolutize = { version = "3.0.13", features = ["once_cell_cache"] }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "7d21c6923a506e79cc041708d83cef925efd33f4" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "966a80597d626a9a47eaec78471164422d341453" }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" }
toml = { version = "0.5.9" }

241
README.md
View File

@@ -22,9 +22,7 @@ An extremely fast Python linter, written in Rust.
- 📦 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired cache support
- 🔧 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired `--fix` support
- 👀 [TypeScript](https://www.typescriptlang.org/docs/handbook/configuring-watch.html)-inspired `--watch` support
_ruff is a proof-of-concept and not yet intended for production use. It supports only a small subset
of the Flake8 rules, and may crash on your codebase._
- ⚖️ [Near-complete parity](#Parity-with-Flake8) with the built-in Flake8 rule set
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -88,7 +86,7 @@ ruff path/to/code/ --select F401 F403
See `ruff --help` for more:
```shell
ruff (v0.0.40)
ruff (v0.0.45) 0.0.45
An extremely fast Python linter.
USAGE:
@@ -98,12 +96,20 @@ ARGS:
<FILES>...
OPTIONS:
-e, --exit-zero
Exit with status code "0", even upon detecting errors
--select <SELECT>...
List of error codes to enable
--extend-select <EXTEND_SELECT>...
Like --select, but adds additional error codes on top of the selected ones
--ignore <IGNORE>...
List of error codes to ignore
--extend-ignore <EXTEND_IGNORE>...
Like --ignore, but adds additional error codes on top of the ignored ones
--exclude <EXCLUDE>...
List of paths, used to exclude files and/or directories from checks
--extend-exclude <EXTEND_EXCLUDE>...
Like --exclude, but adds additional files and directories on top of the excluded ones
-e, --exit-zero
Exit with status code "0", even upon detecting errors
-f, --fix
Attempt to automatically fix lint errors
--format <FORMAT>
@@ -111,14 +117,16 @@ OPTIONS:
json]
-h, --help
Print help information
--ignore <IGNORE>...
List of error codes to ignore
-n, --no-cache
Disable cache reads
-q, --quiet
Disable all logging (but still exit with status code "1" upon detecting errors)
--select <SELECT>...
List of error codes to enable
--add-noqa
Enable automatic additions of noqa directives to failing lines
--show-files
See the files ruff will be run against with the current settings
--show-settings
See ruff's settings
-v, --verbose
Enable verbose logging
-V, --version
@@ -127,6 +135,8 @@ OPTIONS:
Run in watch mode by re-running whenever files change
```
### Excluding files
Exclusions are based on globs, and can be either:
- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the
@@ -136,34 +146,71 @@ Exclusions are based on globs, and can be either:
(to exclude any Python files in `directory`). Note that these paths are relative to the
project root (e.g., the directory containing your `pyproject.toml`).
### Ignoring errors
To omit a lint check entirely, add it to the "ignore" list via `--ignore` or `--extend-ignore`,
either on the command-line or in your `project.toml` file.
To ignore an error in-line, ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
To ignore an individual error, add `# noqa: {code}` to the end of the line, like so:
```python
# Ignore F841.
x = 1 # noqa: F841
# Ignore E741 and F841.
i = 1 # noqa: E741, F841
# Ignore _all_ errors.
x = 1 # noqa
```
Note that, for multi-line strings, the `noqa` directive should come at the end of the string, and
will apply to the entire body, like so:
```python
"""Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa: E501
```
ruff supports several (experimental) 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.**
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.**
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.**
### Compatibility with Black
ruff is intended to be compatible with [Black](https://github.com/psf/black), and should be
compatible out-of-the-box as long as the `line-length` setting is consistent between the two.
ruff is compatible with [Black](https://github.com/psf/black) out-of-the-box, as long as
the `line-length` setting is consistent between the two.
As a project, ruff is designed to be used alongside Black and, as such, will defer implementing
lint rules that are obviated by Black (e.g., stylistic rules).
stylistic lint rules that are obviated by autoformatting.
### Parity with Flake8
ruff's goal is to achieve feature-parity with Flake8 when used (1) without any plugins,
(2) alongside Black, and (3) on Python 3 code. (Using Black obviates the need for many of Flake8's
stylistic checks; limiting to Python 3 obviates the need for certain compatibility checks.)
ruff's goal is to achieve feature-parity with Flake8 when used (1) without plugins, (2) alongside
Black, and (3) on Python 3 code.
Under those conditions, Flake8 implements about 60 rules, give or take. At time of writing, ruff
implements 42 rules. (Note that these 42 rules likely cover a disproportionate share of errors:
unused imports, undefined variables, etc.)
The unimplemented rules are tracked in #170, and include:
- 14 rules related to string `.format` calls.
- 4 logical rules.
- 1 rule related to parsing.
**Under those conditions, ruff implements 44 out of 60 rules.** (ruff is missing: 14 rules related
to string `.format` calls, 1 rule related to docstring parsing, and 1 rule related to redefined
variables.)
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
1. Flake8 supports a wider range of `noqa` patterns, such as per-file ignores defined in `.flake8`.
2. Flake8 has a plugin architecture and supports writing custom lint rules.
1. Flake8 has a plugin architecture and supports writing custom lint rules.
2. Flake8 supports a wider range of `noqa` patterns, such as per-file ignores defined in `.flake8`.
3. ruff does not yet support parenthesized context managers.
## Rules
@@ -182,11 +229,13 @@ Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis F
| E741 | AmbiguousVariableName | ambiguous variable name '...' |
| E742 | AmbiguousClassName | ambiguous class name '...' |
| E743 | AmbiguousFunctionName | ambiguous function name '...' |
| E902 | IOError | No such file or directory: `...` |
| E902 | IOError | ... |
| E999 | SyntaxError | SyntaxError: ... |
| F401 | UnusedImport | `...` imported but unused |
| F403 | ImportStarUsage | `from ... import *` used; unable to detect undefined names |
| F402 | ImportShadowedByLoopVar | import '...' from line 1 shadowed by loop variable |
| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names |
| F404 | LateFutureImport | from __future__ imports must occur at the beginning of the file |
| F405 | ImportStarUsage | '...' may be undefined, or defined from star imports: ... |
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level |
| F407 | FutureFeatureNotDefined | future feature '...' is not defined |
| F541 | FStringMissingPlaceholders | f-string without any placeholders |
@@ -212,6 +261,7 @@ Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis F
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` |
| R001 | UselessObjectInheritance | Class `...` inherits from object |
| R002 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead |
| M001 | UnusedNOQA | Unused `noqa` directive |
## Development
@@ -247,29 +297,29 @@ Add this `pyproject.toml` to the CPython directory:
```toml
[tool.ruff]
line-length = 88
exclude = [
"./resources/test/cpython/Lib/lib2to3/tests/data/bom.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/crlf.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/different_encoding.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/false_encoding.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/py2_test_grammar.py",
"./resources/test/cpython/Lib/test/bad_coding2.py",
"./resources/test/cpython/Lib/test/badsyntax_3131.py",
"./resources/test/cpython/Lib/test/badsyntax_pep3120.py",
"./resources/test/cpython/Lib/test/encoded_modules/module_iso_8859_1.py",
"./resources/test/cpython/Lib/test/encoded_modules/module_koi8_r.py",
"./resources/test/cpython/Lib/test/test_fstring.py",
"./resources/test/cpython/Lib/test/test_grammar.py",
"./resources/test/cpython/Lib/test/test_importlib/test_util.py",
"./resources/test/cpython/Lib/test/test_named_expressions.py",
"./resources/test/cpython/Lib/test/test_patma.py",
"./resources/test/cpython/Lib/test/test_source_encoding.py",
"./resources/test/cpython/Tools/c-analyzer/c_parser/parser/_delim.py",
"./resources/test/cpython/Tools/i18n/pygettext.py",
"./resources/test/cpython/Tools/test2to3/maintest.py",
"./resources/test/cpython/Tools/test2to3/setup.py",
"./resources/test/cpython/Tools/test2to3/test/test_foo.py",
"./resources/test/cpython/Tools/test2to3/test2to3/hello.py",
extend-exclude = [
"Lib/lib2to3/tests/data/bom.py",
"Lib/lib2to3/tests/data/crlf.py",
"Lib/lib2to3/tests/data/different_encoding.py",
"Lib/lib2to3/tests/data/false_encoding.py",
"Lib/lib2to3/tests/data/py2_test_grammar.py",
"Lib/test/bad_coding2.py",
"Lib/test/badsyntax_3131.py",
"Lib/test/badsyntax_pep3120.py",
"Lib/test/encoded_modules/module_iso_8859_1.py",
"Lib/test/encoded_modules/module_koi8_r.py",
"Lib/test/test_fstring.py",
"Lib/test/test_grammar.py",
"Lib/test/test_importlib/test_util.py",
"Lib/test/test_named_expressions.py",
"Lib/test/test_patma.py",
"Lib/test/test_source_encoding.py",
"Tools/c-analyzer/c_parser/parser/_delim.py",
"Tools/i18n/pygettext.py",
"Tools/test2to3/maintest.py",
"Tools/test2to3/setup.py",
"Tools/test2to3/test/test_foo.py",
"Tools/test2to3/test2to3/hello.py",
]
```
@@ -278,17 +328,21 @@ Next, to benchmark the release build:
```shell
cargo build --release
hyperfine --ignore-failure --warmup 1 \
hyperfine --ignore-failure --warmup 10 --runs 100 \
"./target/release/ruff ./resources/test/cpython/ --no-cache" \
"./target/release/ruff ./resources/test/cpython/"
Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
Time (mean ± σ): 353.6 ms ± 7.6 ms [User: 2868.8 ms, System: 171.5 ms]
Range (min … max): 344.4 ms … 367.3 ms 10 runs
Time (mean ± σ): 297.4 ms ± 4.9 ms [User: 2460.0 ms, System: 67.2 ms]
Range (min … max): 287.7 ms … 312.1 ms 100 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: ./target/release/ruff ./resources/test/cpython/
Time (mean ± σ): 59.6 ms ± 2.5 ms [User: 36.4 ms, System: 345.6 ms]
Range (min … max): 55.9 ms … 67.0 ms 48 runs
Time (mean ± σ): 79.6 ms ± 7.3 ms [User: 59.7 ms, System: 356.1 ms]
Range (min … max): 62.4 ms … 111.2 ms 100 runs
Warning: Ignoring non-zero exit code.
```
To benchmark against the ecosystem's existing tools:
@@ -300,11 +354,8 @@ hyperfine --ignore-failure --warmup 5 \
"pyflakes resources/test/cpython" \
"autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython" \
"pycodestyle resources/test/cpython" \
"pycodestyle --select E501 resources/test/cpython" \
"flake8 resources/test/cpython" \
"flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython" \
"python -m scripts.run_flake8 resources/test/cpython" \
"python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501"
"python -m scripts.run_flake8 resources/test/cpython"
```
In order, these evaluate:
@@ -314,66 +365,58 @@ In order, these evaluate:
- PyFlakes
- autoflake
- pycodestyle
- pycodestyle, limited to the checks supported by ruff
- Flake8
- Flake8, limited to the checks supported by ruff
- Flake8, with a hack to enable multiprocessing on macOS
- Flake8, with a hack to enable multiprocessing on macOS, limited to the checks supported by ruff
(You can `poetry install` from `./scripts` to create a working environment for the above.)
```shell
Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
Time (mean ± σ): 469.3 ms ± 16.3 ms [User: 2663.0 ms, System: 972.5 ms]
Range (min … max): 445.2 ms … 494.8 ms 10 runs
Time (mean ± σ): 297.9 ms ± 7.0 ms [User: 2436.6 ms, System: 65.9 ms]
Range (min … max): 289.9 ms … 314.6 ms 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: pylint --recursive=y resources/test/cpython/
Time (mean ± σ): 27.211 s ± 0.097 s [User: 26.405 s, System: 0.799 s]
Range (min … max): 27.056 s … 27.349 s 10 runs
Time (mean ± σ): 37.634 s ± 0.225 s [User: 36.728 s, System: 0.853 s]
Range (min … max): 37.201 s … 38.106 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 3: pyflakes resources/test/cpython
Time (mean ± σ): 27.309 s ± 0.033 s [User: 27.137 s, System: 0.169 s]
Range (min … max): 27.267 s … 27.372 s 10 runs
Time (mean ± σ): 40.950 s ± 0.449 s [User: 40.688 s, System: 0.229 s]
Range (min … max): 40.348 s … 41.671 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 4: autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython
Time (mean ± σ): 8.027 s ± 0.024 s [User: 74.255 s, System: 0.953 s]
Range (min … max): 7.969 s … 8.052 s 10 runs
Time (mean ± σ): 11.562 s ± 0.160 s [User: 107.022 s, System: 1.143 s]
Range (min … max): 11.417 s … 11.917 s 10 runs
Benchmark 5: pycodestyle resources/test/cpython
Time (mean ± σ): 41.666 s ± 0.266 s [User: 41.531 s, System: 0.132 s]
Range (min … max): 41.295 s … 41.980 s 10 runs
Time (mean ± σ): 67.428 s ± 0.985 s [User: 67.199 s, System: 0.203 s]
Range (min … max): 65.313 s … 68.496 s 10 runs
Benchmark 6: pycodestyle --select E501 resources/test/cpython
Time (mean ± σ): 14.547 s ± 0.077 s [User: 14.466 s, System: 0.079 s]
Range (min … max): 14.429 s … 14.695 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 7: flake8 resources/test/cpython
Time (mean ± σ): 75.700 s ± 0.152 s [User: 75.254 s, System: 0.440 s]
Range (min … max): 75.513 s … 76.014 s 10 runs
Benchmark 6: flake8 resources/test/cpython
Time (mean ± σ): 116.099 s ± 1.178 s [User: 115.217 s, System: 0.845 s]
Range (min … max): 114.180 s … 117.724 s 10 runs
Benchmark 8: flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython
Time (mean ± σ): 75.122 s ± 0.532 s [User: 74.677 s, System: 0.440 s]
Range (min … max): 74.130 s … 75.606 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 9: python -m scripts.run_flake8 resources/test/cpython
Time (mean ± σ): 12.794 s ± 0.147 s [User: 90.792 s, System: 0.738 s]
Range (min … max): 12.606 s … 13.030 s 10 runs
Benchmark 10: python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501
Time (mean ± σ): 12.487 s ± 0.118 s [User: 90.052 s, System: 0.714 s]
Range (min … max): 12.265 s … 12.665 s 10 runs
Benchmark 7: python -m scripts.run_flake8 resources/test/cpython
Time (mean ± σ): 20.477 s ± 0.349 s [User: 142.372 s, System: 1.504 s]
Range (min … max): 20.107 s … 21.183 s 10 runs
Summary
'./target/release/ruff ./resources/test/cpython/ --no-cache' ran
17.10 ± 0.60 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
26.60 ± 0.96 times faster than 'python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501'
27.26 ± 1.00 times faster than 'python -m scripts.run_flake8 resources/test/cpython'
30.99 ± 1.09 times faster than 'pycodestyle --select E501 resources/test/cpython'
57.98 ± 2.03 times faster than 'pylint --recursive=y resources/test/cpython/'
58.19 ± 2.02 times faster than 'pyflakes resources/test/cpython'
88.77 ± 3.14 times faster than 'pycodestyle resources/test/cpython'
160.06 ± 5.68 times faster than 'flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython'
161.29 ± 5.61 times faster than 'flake8 resources/test/cpython'
38.81 ± 1.05 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
68.74 ± 1.99 times faster than 'python -m scripts.run_flake8 resources/test/cpython'
126.33 ± 3.05 times faster than 'pylint --recursive=y resources/test/cpython/'
137.46 ± 3.55 times faster than 'pyflakes resources/test/cpython'
226.35 ± 6.23 times faster than 'pycodestyle resources/test/cpython'
389.73 ± 9.92 times faster than 'flake8 resources/test/cpython'
```
## License

View File

@@ -5,5 +5,12 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
"""
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa: E501
_ = "---------------------------------------------------------------------------AAAAAAA"
_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜"

9
resources/test/fixtures/F402.py vendored Normal file
View File

@@ -0,0 +1,9 @@
import os
import os.path as path
for os in range(3):
pass
for path in range(3):
pass

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

@@ -0,0 +1,11 @@
from mymodule import *
def print_name():
print(name)
def print_name(name):
print(name)
__all__ = ['a']

View File

@@ -82,3 +82,10 @@ class Ticket:
def update_tomato():
print(TOMATO)
TOMATO = "cherry tomato"
A = f'{B}'
A = (
f'B'
f'{B}'
)

57
resources/test/fixtures/M001.py vendored Normal file
View File

@@ -0,0 +1,57 @@
def f() -> None:
# Valid
a = 1 # noqa
# Valid
b = 2 # noqa: F841
# Invalid
c = 1 # noqa
print(c)
# Invalid
d = 1 # noqa: E501
# Invalid
d = 1 # noqa: F841, E501
# Valid
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa: E501
# Valid
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa
# Invalid
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa: E501, F841
# Invalid
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
""" # noqa: E501
# Invalid
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
""" # noqa

View File

@@ -7,7 +7,7 @@ use rustpython_parser::ast::{
};
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::{Binding, BindingKind, FunctionScope, Scope, ScopeKind};
use crate::ast::types::{Binding, BindingKind, CheckLocator, FunctionScope, Scope, ScopeKind};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckKind, Fix, RejectedCmpop};
@@ -37,6 +37,7 @@ pub fn check_not_tests(
operand: &Expr,
check_not_in: bool,
check_not_is: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -46,12 +47,18 @@ pub fn check_not_tests(
match op {
Cmpop::In => {
if check_not_in {
checks.push(Check::new(CheckKind::NotInTest, operand.location));
checks.push(Check::new(
CheckKind::NotInTest,
locator.locate_check(operand.location),
));
}
}
Cmpop::Is => {
if check_not_is {
checks.push(Check::new(CheckKind::NotIsTest, operand.location));
checks.push(Check::new(
CheckKind::NotIsTest,
locator.locate_check(operand.location),
));
}
}
_ => {}
@@ -64,7 +71,7 @@ pub fn check_not_tests(
}
/// Check UnusedVariable compliance.
pub fn check_unused_variables(scope: &Scope) -> Vec<Check> {
pub fn check_unused_variables(scope: &Scope, locator: &dyn CheckLocator) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
if matches!(
@@ -85,7 +92,7 @@ pub fn check_unused_variables(scope: &Scope) -> Vec<Check> {
{
checks.push(Check::new(
CheckKind::UnusedVariable(name.to_string()),
binding.location,
locator.locate_check(binding.location),
));
}
}
@@ -299,6 +306,7 @@ pub fn check_repeated_keys(
keys: &Vec<Expr>,
check_repeated_literals: bool,
check_repeated_variables: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -313,7 +321,7 @@ pub fn check_repeated_keys(
if check_repeated_literals && v1 == v2 {
checks.push(Check::new(
CheckKind::MultiValueRepeatedKeyLiteral,
k2.location,
locator.locate_check(k2.location),
))
}
}
@@ -321,7 +329,7 @@ pub fn check_repeated_keys(
if check_repeated_variables && v1 == v2 {
checks.push(Check::new(
CheckKind::MultiValueRepeatedKeyVariable((*v2).to_string()),
k2.location,
locator.locate_check(k2.location),
))
}
}
@@ -340,6 +348,7 @@ pub fn check_literal_comparisons(
comparators: &Vec<Expr>,
check_none_comparisons: bool,
check_true_false_comparisons: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -359,13 +368,13 @@ pub fn check_literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::Eq),
comparator.location,
locator.locate_check(comparator.location),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
comparator.location,
locator.locate_check(comparator.location),
));
}
}
@@ -379,13 +388,13 @@ pub fn check_literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
comparator.location,
locator.locate_check(comparator.location),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
comparator.location,
locator.locate_check(comparator.location),
));
}
}
@@ -405,13 +414,13 @@ pub fn check_literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::Eq),
comparator.location,
locator.locate_check(comparator.location),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
comparator.location,
locator.locate_check(comparator.location),
));
}
}
@@ -425,13 +434,13 @@ pub fn check_literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
comparator.location,
locator.locate_check(comparator.location),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
comparator.location,
locator.locate_check(comparator.location),
));
}
}
@@ -528,9 +537,9 @@ pub fn check_type_comparison(
/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance.
pub fn check_starred_expressions(
elts: &[Expr],
location: Location,
check_too_many_expressions: bool,
check_two_starred_expressions: bool,
location: Location,
) -> Option<Check> {
let mut has_starred: bool = false;
let mut starred_index: Option<usize> = None;
@@ -563,6 +572,7 @@ pub fn check_break_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
locator: &dyn CheckLocator,
) -> Option<Check> {
let mut allowed: bool = false;
let mut parent = stmt;
@@ -589,7 +599,10 @@ pub fn check_break_outside_loop(
}
if !allowed {
Some(Check::new(CheckKind::BreakOutsideLoop, stmt.location))
Some(Check::new(
CheckKind::BreakOutsideLoop,
locator.locate_check(stmt.location),
))
} else {
None
}
@@ -600,6 +613,7 @@ pub fn check_continue_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
locator: &dyn CheckLocator,
) -> Option<Check> {
let mut allowed: bool = false;
let mut parent = stmt;
@@ -626,7 +640,10 @@ pub fn check_continue_outside_loop(
}
if !allowed {
Some(Check::new(CheckKind::ContinueOutsideLoop, stmt.location))
Some(Check::new(
CheckKind::ContinueOutsideLoop,
locator.locate_check(stmt.location),
))
} else {
None
}

View File

@@ -25,6 +25,7 @@ pub enum ScopeKind {
pub struct Scope {
pub id: usize,
pub kind: ScopeKind,
pub import_starred: bool,
pub values: BTreeMap<String, Binding>,
}
@@ -33,6 +34,7 @@ impl Scope {
Scope {
id: id(),
kind,
import_starred: false,
values: BTreeMap::new(),
}
}
@@ -44,6 +46,7 @@ pub enum BindingKind {
Argument,
Assignment,
Binding,
LoopVar,
Builtin,
ClassDefinition,
Definition,
@@ -62,3 +65,7 @@ pub struct Binding {
/// last used.
pub used: Option<(usize, Location)>,
}
pub trait CheckLocator {
fn locate_check(&self, default: Location) -> Location;
}

View File

@@ -9,7 +9,7 @@ use rustpython_parser::parser;
use crate::ast::operations::{extract_all_names, SourceCodeLocator};
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{Binding, BindingKind, FunctionScope, Scope, ScopeKind};
use crate::ast::types::{Binding, BindingKind, CheckLocator, FunctionScope, Scope, ScopeKind};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{checks, operations, visitor};
use crate::autofix::fixer;
@@ -42,7 +42,7 @@ struct Checker<'a> {
deferred_lambdas: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
deferred_assignments: Vec<usize>,
// Derivative state.
in_f_string: bool,
in_f_string: Option<Location>,
in_annotation: bool,
in_literal: bool,
seen_non_import: bool,
@@ -74,7 +74,7 @@ impl<'a> Checker<'a> {
deferred_functions: vec![],
deferred_lambdas: vec![],
deferred_assignments: vec![],
in_f_string: false,
in_f_string: None,
in_annotation: false,
in_literal: false,
seen_non_import: false,
@@ -171,7 +171,6 @@ where
// Pre-visit.
match &stmt.node {
StmtKind::Global { names } | StmtKind::Nonlocal { names } => {
// TODO(charlie): Handle doctests.
let global_scope_id = self.scopes[GLOBAL_SCOPE_INDEX].id;
let current_scope =
@@ -193,25 +192,34 @@ where
}
if self.settings.select.contains(&CheckCode::E741) {
self.checks.extend(names.iter().filter_map(|name| {
checks::check_ambiguous_variable_name(name, stmt.location)
}));
let location = self.locate_check(stmt.location);
self.checks.extend(
names.iter().filter_map(|name| {
checks::check_ambiguous_variable_name(name, location)
}),
);
}
}
StmtKind::Break => {
if self.settings.select.contains(&CheckCode::F701) {
if let Some(check) =
checks::check_break_outside_loop(stmt, &self.parents, &self.parent_stack)
{
if let Some(check) = checks::check_break_outside_loop(
stmt,
&self.parents,
&self.parent_stack,
self,
) {
self.checks.push(check);
}
}
}
StmtKind::Continue => {
if self.settings.select.contains(&CheckCode::F702) {
if let Some(check) =
checks::check_continue_outside_loop(stmt, &self.parents, &self.parent_stack)
{
if let Some(check) = checks::check_continue_outside_loop(
stmt,
&self.parents,
&self.parent_stack,
self,
) {
self.checks.push(check);
}
}
@@ -231,8 +239,10 @@ where
..
} => {
if self.settings.select.contains(&CheckCode::E743) {
if let Some(check) = checks::check_ambiguous_function_name(name, stmt.location)
{
if let Some(check) = checks::check_ambiguous_function_name(
name,
self.locate_check(stmt.location),
) {
self.checks.push(check);
}
}
@@ -293,7 +303,7 @@ where
ScopeKind::Class | ScopeKind::Module => {
self.checks.push(Check::new(
CheckKind::ReturnOutsideFunction,
stmt.location,
self.locate_check(stmt.location),
));
}
_ => {}
@@ -325,7 +335,9 @@ where
}
if self.settings.select.contains(&CheckCode::E742) {
if let Some(check) = checks::check_ambiguous_class_name(name, stmt.location) {
if let Some(check) =
checks::check_ambiguous_class_name(name, self.locate_check(stmt.location))
{
self.checks.push(check);
}
}
@@ -351,7 +363,7 @@ where
{
self.checks.push(Check::new(
CheckKind::ModuleImportNotAtTopOfFile,
stmt.location,
self.locate_check(stmt.location),
));
}
@@ -391,7 +403,11 @@ where
}
}
}
StmtKind::ImportFrom { names, module, .. } => {
StmtKind::ImportFrom {
names,
module,
level,
} => {
if self
.settings
.select
@@ -401,7 +417,7 @@ where
{
self.checks.push(Check::new(
CheckKind::ModuleImportNotAtTopOfFile,
stmt.location,
self.locate_check(stmt.location),
));
}
@@ -437,18 +453,26 @@ where
{
self.checks.push(Check::new(
CheckKind::FutureFeatureNotDefined(alias.node.name.to_string()),
stmt.location,
self.locate_check(stmt.location),
));
}
if self.settings.select.contains(&CheckCode::F404) && !self.futures_allowed
{
self.checks
.push(Check::new(CheckKind::LateFutureImport, stmt.location));
self.checks.push(Check::new(
CheckKind::LateFutureImport,
self.locate_check(stmt.location),
));
}
} else if alias.node.name == "*" {
let module_name = format!(
"{}{}",
".".repeat(level.unwrap_or_default()),
module.clone().unwrap_or_else(|| "module".to_string()),
);
self.add_binding(
name,
module_name.to_string(),
Binding {
kind: BindingKind::StarImportation,
used: None,
@@ -456,27 +480,29 @@ where
},
);
if self.settings.select.contains(&CheckCode::F403) {
self.checks.push(Check::new(
CheckKind::ImportStarUsage(
module.clone().unwrap_or_else(|| "module".to_string()),
),
stmt.location,
));
}
if self.settings.select.contains(&CheckCode::F406) {
let scope = &self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
if !matches!(scope.kind, ScopeKind::Module) {
self.checks.push(Check::new(
CheckKind::ImportStarNotPermitted(
module.clone().unwrap_or_else(|| "module".to_string()),
),
stmt.location,
CheckKind::ImportStarNotPermitted(module_name.to_string()),
self.locate_check(stmt.location),
));
}
}
if self.settings.select.contains(&CheckCode::F403) {
self.checks.push(Check::new(
CheckKind::ImportStarUsed(module_name.to_string()),
self.locate_check(stmt.location),
));
}
let scope = &mut self.scopes[*(self
.scope_stack
.last_mut()
.expect("No current scope found."))];
scope.import_starred = true;
} else {
let binding = Binding {
kind: BindingKind::Importation(match module {
@@ -504,14 +530,18 @@ where
}
StmtKind::If { test, .. } => {
if self.settings.select.contains(&CheckCode::F634) {
if let Some(check) = checks::check_if_tuple(test, stmt.location) {
if let Some(check) =
checks::check_if_tuple(test, self.locate_check(stmt.location))
{
self.checks.push(check);
}
}
}
StmtKind::Assert { test, .. } => {
if self.settings.select.contains(CheckKind::AssertTuple.code()) {
if let Some(check) = checks::check_assert_tuple(test, stmt.location) {
if let Some(check) =
checks::check_assert_tuple(test, self.locate_check(stmt.location))
{
self.checks.push(check);
}
}
@@ -525,7 +555,9 @@ where
}
StmtKind::Assign { value, .. } => {
if self.settings.select.contains(&CheckCode::E731) {
if let Some(check) = checks::check_do_not_assign_lambda(value, stmt.location) {
if let Some(check) =
checks::check_do_not_assign_lambda(value, self.locate_check(stmt.location))
{
self.checks.push(check);
}
}
@@ -533,9 +565,10 @@ where
StmtKind::AnnAssign { value, .. } => {
if self.settings.select.contains(&CheckCode::E731) {
if let Some(value) = value {
if let Some(check) =
checks::check_do_not_assign_lambda(value, stmt.location)
{
if let Some(check) = checks::check_do_not_assign_lambda(
value,
self.locate_check(stmt.location),
) {
self.checks.push(check);
}
}
@@ -590,7 +623,6 @@ where
let prev_in_literal = self.in_literal;
let prev_in_annotation = self.in_annotation;
// Important:
if self.in_annotation && self.annotations_future_enabled {
self.deferred_annotations.push((
expr,
@@ -616,9 +648,9 @@ where
self.settings.select.contains(&CheckCode::F622);
if let Some(check) = checks::check_starred_expressions(
elts,
expr.location,
check_too_many_expressions,
check_two_starred_expressions,
self.locate_check(expr.location),
) {
self.checks.push(check);
}
@@ -628,9 +660,10 @@ where
ExprContext::Load => self.handle_node_load(expr),
ExprContext::Store => {
if self.settings.select.contains(&CheckCode::E741) {
if let Some(check) =
checks::check_ambiguous_variable_name(id, expr.location)
{
if let Some(check) = checks::check_ambiguous_variable_name(
id,
self.locate_check(expr.location),
) {
self.checks.push(check);
}
}
@@ -661,18 +694,6 @@ where
}
}
}
//
// if id == "locals" {
// let scope = &self.scopes
// [*(self.scope_stack.last().expect("No current scope found."))];
// if matches!(scope.kind, ScopeKind::Function(_)) {
// let parent =
// self.parents[*(self.parent_stack.last().expect("No parent found."))];
// if matches!(parent.node, StmtKind::Call)
// }
//
// }
}
ExprKind::Dict { keys, .. } => {
let check_repeated_literals = self.settings.select.contains(&CheckCode::F601);
@@ -682,6 +703,7 @@ where
keys,
check_repeated_literals,
check_repeated_variables,
self,
));
}
}
@@ -694,12 +716,14 @@ where
.contains(CheckKind::YieldOutsideFunction.code())
&& matches!(scope.kind, ScopeKind::Class | ScopeKind::Module)
{
self.checks
.push(Check::new(CheckKind::YieldOutsideFunction, expr.location));
self.checks.push(Check::new(
CheckKind::YieldOutsideFunction,
self.locate_check(expr.location),
));
}
}
ExprKind::JoinedStr { values } => {
if !self.in_f_string
if self.in_f_string.is_none()
&& self
.settings
.select
@@ -710,10 +734,10 @@ where
{
self.checks.push(Check::new(
CheckKind::FStringMissingPlaceholders,
expr.location,
self.locate_check(expr.location),
));
}
self.in_f_string = true;
self.in_f_string = Some(expr.location);
}
ExprKind::BinOp {
left,
@@ -746,6 +770,7 @@ where
operand,
check_not_in,
check_not_is,
self,
));
}
}
@@ -763,6 +788,7 @@ where
comparators,
check_none_comparisons,
check_true_false_comparisons,
self,
));
}
@@ -771,7 +797,7 @@ where
left,
ops,
comparators,
expr.location,
self.locate_check(expr.location),
));
}
@@ -779,7 +805,7 @@ where
self.checks.extend(checks::check_type_comparison(
ops,
comparators,
expr.location,
self.locate_check(expr.location),
));
}
}
@@ -944,9 +970,10 @@ where
match name {
Some(name) => {
if self.settings.select.contains(&CheckCode::E741) {
if let Some(check) =
checks::check_ambiguous_variable_name(name, excepthandler.location)
{
if let Some(check) = checks::check_ambiguous_variable_name(
name,
self.locate_check(excepthandler.location),
) {
self.checks.push(check);
}
}
@@ -1046,14 +1073,22 @@ where
);
if self.settings.select.contains(&CheckCode::E741) {
if let Some(check) = checks::check_ambiguous_variable_name(&arg.node.arg, arg.location)
{
if let Some(check) = checks::check_ambiguous_variable_name(
&arg.node.arg,
self.locate_check(arg.location),
) {
self.checks.push(check);
}
}
}
}
impl CheckLocator for Checker<'_> {
fn locate_check(&self, default: Location) -> Location {
self.in_f_string.unwrap_or(default)
}
}
impl<'a> Checker<'a> {
fn push_parent(&mut self, parent: &'a Stmt) {
self.parent_stack.push(self.parents.len());
@@ -1110,12 +1145,24 @@ impl<'a> Checker<'a> {
// TODO(charlie): Don't treat annotations as assignments if there is an existing value.
let binding = match scope.values.get(&name) {
None => binding,
Some(existing) => Binding {
kind: binding.kind,
location: binding.location,
used: existing.used,
},
Some(existing) => {
if self.settings.select.contains(&CheckCode::F402)
&& matches!(existing.kind, BindingKind::Importation(_))
&& matches!(binding.kind, BindingKind::LoopVar)
{
self.checks.push(Check::new(
CheckKind::ImportShadowedByLoopVar(name.clone(), existing.location.row()),
binding.location,
));
}
Binding {
kind: binding.kind,
location: binding.location,
used: existing.used,
}
}
};
scope.values.insert(name, binding);
}
@@ -1126,6 +1173,7 @@ impl<'a> Checker<'a> {
let mut first_iter = true;
let mut in_generator = false;
let mut import_starred = false;
for scope_index in self.scope_stack.iter().rev() {
let scope = &mut self.scopes[*scope_index];
if matches!(scope.kind, ScopeKind::Class) {
@@ -1142,6 +1190,28 @@ impl<'a> Checker<'a> {
first_iter = false;
in_generator = matches!(scope.kind, ScopeKind::Generator);
import_starred = import_starred || scope.import_starred;
}
if import_starred {
if self.settings.select.contains(&CheckCode::F405) {
let mut from_list = vec![];
for scope_index in self.scope_stack.iter().rev() {
let scope = &self.scopes[*scope_index];
for (name, binding) in scope.values.iter() {
if matches!(binding.kind, BindingKind::StarImportation) {
from_list.push(name.as_str());
}
}
}
from_list.sort();
self.checks.push(Check::new(
CheckKind::ImportStarUsage(id.clone(), from_list.join(", ")),
self.locate_check(expr.location),
));
}
return;
}
if self.settings.select.contains(&CheckCode::F821) {
@@ -1151,7 +1221,7 @@ impl<'a> Checker<'a> {
}
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
expr.location,
self.locate_check(expr.location),
))
}
}
@@ -1173,7 +1243,7 @@ impl<'a> Checker<'a> {
if scope_id == current.id {
self.checks.push(Check::new(
CheckKind::UndefinedLocal(id.clone()),
location,
self.locate_check(location),
));
}
}
@@ -1198,8 +1268,19 @@ impl<'a> Checker<'a> {
if matches!(
parent.node,
StmtKind::For { .. } | StmtKind::AsyncFor { .. }
) || operations::is_unpacking_assignment(parent)
{
) {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::LoopVar,
used: None,
location: expr.location,
},
);
return;
}
if operations::is_unpacking_assignment(parent) {
self.add_binding(
id.to_string(),
Binding {
@@ -1255,7 +1336,7 @@ impl<'a> Checker<'a> {
{
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
expr.location,
self.locate_check(expr.location),
))
}
}
@@ -1280,7 +1361,7 @@ impl<'a> Checker<'a> {
} else if self.settings.select.contains(&CheckCode::F722) {
self.checks.push(Check::new(
CheckKind::ForwardAnnotationSyntaxError(expression.to_string()),
location,
self.locate_check(location),
));
}
}
@@ -1332,17 +1413,18 @@ impl<'a> Checker<'a> {
}
fn check_deferred_assignments(&mut self) {
while let Some(index) = self.deferred_assignments.pop() {
if self.settings.select.contains(&CheckCode::F841) {
if self.settings.select.contains(&CheckCode::F841) {
while let Some(index) = self.deferred_assignments.pop() {
self.checks
.extend(checks::check_unused_variables(&self.scopes[index]));
.extend(checks::check_unused_variables(&self.scopes[index], self));
}
}
}
fn check_dead_scopes(&mut self) {
if !self.settings.select.contains(&CheckCode::F822)
&& !self.settings.select.contains(&CheckCode::F401)
if !self.settings.select.contains(&CheckCode::F401)
&& !self.settings.select.contains(&CheckCode::F405)
&& !self.settings.select.contains(&CheckCode::F822)
{
return;
}
@@ -1357,15 +1439,39 @@ impl<'a> Checker<'a> {
});
if self.settings.select.contains(&CheckCode::F822)
&& !scope.import_starred
&& !self.path.ends_with("__init__.py")
{
if let Some(binding) = all_binding {
if let Some(all_binding) = all_binding {
if let Some(names) = all_names {
for name in names {
if !scope.values.contains_key(name) {
self.checks.push(Check::new(
CheckKind::UndefinedExport(name.to_string()),
binding.location,
self.locate_check(all_binding.location),
));
}
}
}
}
}
if self.settings.select.contains(&CheckCode::F405) && scope.import_starred {
if let Some(all_binding) = all_binding {
if let Some(names) = all_names {
let mut from_list = vec![];
for (name, binding) in scope.values.iter() {
if matches!(binding.kind, BindingKind::StarImportation) {
from_list.push(name.as_str());
}
}
from_list.sort();
for name in names {
if !scope.values.contains_key(name) {
self.checks.push(Check::new(
CheckKind::ImportStarUsage(name.clone(), from_list.join(", ")),
self.locate_check(all_binding.location),
));
}
}
@@ -1386,7 +1492,7 @@ impl<'a> Checker<'a> {
| BindingKind::SubmoduleImportation(full_name) => {
self.checks.push(Check::new(
CheckKind::UnusedImport(full_name.to_string()),
binding.location,
self.locate_check(binding.location),
));
}
_ => {}

View File

@@ -1,6 +1,11 @@
use std::collections::BTreeMap;
use rustpython_parser::ast::Location;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::noqa;
use crate::noqa::Directive;
use crate::settings::Settings;
/// Whether the given line is too long and should be reported.
@@ -19,17 +24,58 @@ fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
}
}
pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings) {
pub fn check_lines(
checks: &mut Vec<Check>,
contents: &str,
noqa_line_for: &[usize],
settings: &Settings,
autofix: &fixer::Mode,
) {
let enforce_line_too_long = settings.select.contains(&CheckCode::E501);
let enforce_noqa = settings.select.contains(&CheckCode::M001);
let mut noqa_directives: BTreeMap<usize, (Directive, Vec<&str>)> = BTreeMap::new();
let mut line_checks = vec![];
let mut ignored = vec![];
for (row, line) in contents.lines().enumerate() {
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)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
if enforce_noqa {
noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
}
// Remove any ignored checks.
// TODO(charlie): Only validate checks for the current line.
for (index, check) in checks.iter().enumerate() {
if check.location.row() == row + 1 && check.is_inline_ignored(line) {
ignored.push(index);
if 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_str());
ignored.push(index)
}
(Directive::Codes(_, codes), matches) => {
if codes.contains(&check.kind.code().as_str()) {
matches.push(check.kind.code().as_str());
ignored.push(index);
}
}
(Directive::None, _) => {}
}
}
}
@@ -37,16 +83,94 @@ pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings)
if enforce_line_too_long {
let line_length = line.chars().count();
if should_enforce_line_length(line, line_length, settings.line_length) {
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
let check = Check::new(
CheckKind::LineTooLong(line_length, settings.line_length),
Location::new(row + 1, settings.line_length + 1),
Location::new(lineno + 1, settings.line_length + 1),
);
if !check.is_inline_ignored(line) {
line_checks.push(check);
match noqa {
(Directive::All(_), matches) => {
matches.push(check.kind.code().as_str());
}
(Directive::Codes(_, codes), matches) => {
if codes.contains(&check.kind.code().as_str()) {
matches.push(check.kind.code().as_str());
} else {
line_checks.push(check);
}
}
(Directive::None, _) => line_checks.push(check),
}
}
}
}
// Enforce that the noqa directive was actually used.
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
match directive {
Directive::All(column) => {
if matches.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(None),
Location::new(row + 1, column + 1),
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "".to_string(),
start: Location::new(row + 1, column + 1),
end: Location::new(row + 1, lines[row].chars().count() + 1),
applied: false,
});
}
line_checks.push(check);
}
}
Directive::Codes(column, codes) => {
let mut invalid_codes = vec![];
let mut valid_codes = vec![];
for code in codes {
if !matches.contains(&code) {
invalid_codes.push(code);
} else {
valid_codes.push(code);
}
}
if !invalid_codes.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(invalid_codes.join(", "))),
Location::new(row + 1, column + 1),
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if valid_codes.is_empty() {
check.amend(Fix {
content: "".to_string(),
start: Location::new(row + 1, column + 1),
end: Location::new(row + 1, lines[row].chars().count() + 1),
applied: false,
});
} else {
check.amend(Fix {
content: format!(" # noqa: {}", valid_codes.join(", ")),
start: Location::new(row + 1, column + 1),
end: Location::new(row + 1, lines[row].chars().count() + 1),
applied: false,
});
}
}
line_checks.push(check);
}
}
Directive::None => {}
}
}
}
ignored.sort();
for index in ignored.iter().rev() {
checks.swap_remove(*index);
@@ -56,22 +180,32 @@ pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings)
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::check_lines;
use super::*;
use std::collections::BTreeSet;
#[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 check_with_max_line_length = |line_length: usize| {
let mut checks: Vec<Check> = vec![];
let settings = Settings {
pyproject: None,
project_root: None,
line_length,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from_iter(vec![CheckCode::E501]),
};
check_lines(&mut checks, line, &settings);
check_lines(
&mut checks,
line,
&noqa_line_for,
&settings,
&fixer::Mode::Generate,
);
return checks;
};
assert!(!check_with_max_line_length(6).is_empty());

View File

@@ -1,12 +1,10 @@
use std::str::FromStr;
use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
pub const ALL_CHECK_CODES: [CheckCode; 42] = [
pub const DEFAULT_CHECK_CODES: [CheckCode; 42] = [
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
@@ -22,8 +20,10 @@ pub const ALL_CHECK_CODES: [CheckCode; 42] = [
CheckCode::E902,
CheckCode::E999,
CheckCode::F401,
CheckCode::F402,
CheckCode::F403,
CheckCode::F404,
CheckCode::F405,
CheckCode::F406,
CheckCode::F407,
CheckCode::F541,
@@ -47,6 +47,52 @@ pub const ALL_CHECK_CODES: [CheckCode; 42] = [
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
];
pub const ALL_CHECK_CODES: [CheckCode; 45] = [
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E721,
CheckCode::E722,
CheckCode::E731,
CheckCode::E741,
CheckCode::E742,
CheckCode::E743,
CheckCode::E902,
CheckCode::E999,
CheckCode::F401,
CheckCode::F402,
CheckCode::F403,
CheckCode::F404,
CheckCode::F405,
CheckCode::F406,
CheckCode::F407,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F632,
CheckCode::F633,
CheckCode::F634,
CheckCode::F701,
CheckCode::F702,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F722,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
CheckCode::M001,
CheckCode::R001,
CheckCode::R002,
];
@@ -68,8 +114,10 @@ pub enum CheckCode {
E902,
E999,
F401,
F402,
F403,
F404,
F405,
F406,
F407,
F541,
@@ -95,6 +143,7 @@ pub enum CheckCode {
F901,
R001,
R002,
M001,
}
impl FromStr for CheckCode {
@@ -117,8 +166,10 @@ impl FromStr for CheckCode {
"E902" => Ok(CheckCode::E902),
"E999" => Ok(CheckCode::E999),
"F401" => Ok(CheckCode::F401),
"F402" => Ok(CheckCode::F402),
"F403" => Ok(CheckCode::F403),
"F404" => Ok(CheckCode::F404),
"F405" => Ok(CheckCode::F405),
"F406" => Ok(CheckCode::F406),
"F407" => Ok(CheckCode::F407),
"F541" => Ok(CheckCode::F541),
@@ -144,6 +195,7 @@ impl FromStr for CheckCode {
"F901" => Ok(CheckCode::F901),
"R001" => Ok(CheckCode::R001),
"R002" => Ok(CheckCode::R002),
"M001" => Ok(CheckCode::M001),
_ => Err(anyhow::anyhow!("Unknown check code: {s}")),
}
}
@@ -167,8 +219,10 @@ impl CheckCode {
CheckCode::E902 => "E902",
CheckCode::E999 => "E999",
CheckCode::F401 => "F401",
CheckCode::F402 => "F402",
CheckCode::F403 => "F403",
CheckCode::F404 => "F404",
CheckCode::F405 => "F405",
CheckCode::F406 => "F406",
CheckCode::F407 => "F407",
CheckCode::F541 => "F541",
@@ -194,13 +248,14 @@ impl CheckCode {
CheckCode::F901 => "F901",
CheckCode::R001 => "R001",
CheckCode::R002 => "R002",
CheckCode::M001 => "M001",
}
}
/// The source for the check (either the AST, the filesystem, or the physical lines).
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E501 => &LintSource::Lines,
CheckCode::E501 | CheckCode::M001 => &LintSource::Lines,
CheckCode::E902 | CheckCode::E999 => &LintSource::FileSystem,
_ => &LintSource::AST,
}
@@ -209,48 +264,51 @@ impl CheckCode {
/// A placeholder representation of the CheckKind for the check.
pub fn kind(&self) -> CheckKind {
match self {
CheckCode::E742 => CheckKind::AmbiguousClassName("...".to_string()),
CheckCode::E743 => CheckKind::AmbiguousFunctionName("...".to_string()),
CheckCode::E741 => CheckKind::AmbiguousVariableName("...".to_string()),
CheckCode::F631 => CheckKind::AssertTuple,
CheckCode::F701 => CheckKind::BreakOutsideLoop,
CheckCode::F702 => CheckKind::ContinueOutsideLoop,
CheckCode::F707 => CheckKind::DefaultExceptNotLast,
CheckCode::E731 => CheckKind::DoNotAssignLambda,
CheckCode::E722 => CheckKind::DoNotUseBareExcept,
CheckCode::F831 => CheckKind::DuplicateArgumentName,
CheckCode::F541 => CheckKind::FStringMissingPlaceholders,
CheckCode::F722 => CheckKind::ForwardAnnotationSyntaxError("...".to_string()),
CheckCode::F407 => CheckKind::FutureFeatureNotDefined("...".to_string()),
CheckCode::E902 => CheckKind::IOError("...".to_string()),
CheckCode::F634 => CheckKind::IfTuple,
CheckCode::F406 => CheckKind::ImportStarNotPermitted("...".to_string()),
CheckCode::F403 => CheckKind::ImportStarUsage("...".to_string()),
CheckCode::F633 => CheckKind::InvalidPrintSyntax,
CheckCode::F632 => CheckKind::IsLiteral,
CheckCode::F404 => CheckKind::LateFutureImport,
CheckCode::E501 => CheckKind::LineTooLong(89, 88),
CheckCode::E402 => CheckKind::ModuleImportNotAtTopOfFile,
CheckCode::F601 => CheckKind::MultiValueRepeatedKeyLiteral,
CheckCode::F602 => CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
CheckCode::R002 => CheckKind::NoAssertEquals,
CheckCode::E501 => CheckKind::LineTooLong(89, 88),
CheckCode::E711 => CheckKind::NoneComparison(RejectedCmpop::Eq),
CheckCode::E712 => CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
CheckCode::E713 => CheckKind::NotInTest,
CheckCode::E714 => CheckKind::NotIsTest,
CheckCode::F901 => CheckKind::RaiseNotImplemented,
CheckCode::F706 => CheckKind::ReturnOutsideFunction,
CheckCode::E999 => CheckKind::SyntaxError("...".to_string()),
CheckCode::F621 => CheckKind::TooManyExpressionsInStarredAssignment,
CheckCode::E712 => CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
CheckCode::F622 => CheckKind::TwoStarredExpressions,
CheckCode::E721 => CheckKind::TypeComparison,
CheckCode::E722 => CheckKind::DoNotUseBareExcept,
CheckCode::E731 => CheckKind::DoNotAssignLambda,
CheckCode::E741 => CheckKind::AmbiguousVariableName("...".to_string()),
CheckCode::E742 => CheckKind::AmbiguousClassName("...".to_string()),
CheckCode::E743 => CheckKind::AmbiguousFunctionName("...".to_string()),
CheckCode::E902 => CheckKind::IOError("...".to_string()),
CheckCode::E999 => CheckKind::SyntaxError("...".to_string()),
CheckCode::F401 => CheckKind::UnusedImport("...".to_string()),
CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1),
CheckCode::F403 => CheckKind::ImportStarUsed("...".to_string()),
CheckCode::F404 => CheckKind::LateFutureImport,
CheckCode::F405 => CheckKind::ImportStarUsage("...".to_string(), "...".to_string()),
CheckCode::F406 => CheckKind::ImportStarNotPermitted("...".to_string()),
CheckCode::F407 => CheckKind::FutureFeatureNotDefined("...".to_string()),
CheckCode::F541 => CheckKind::FStringMissingPlaceholders,
CheckCode::F601 => CheckKind::MultiValueRepeatedKeyLiteral,
CheckCode::F602 => CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
CheckCode::F621 => CheckKind::TooManyExpressionsInStarredAssignment,
CheckCode::F622 => CheckKind::TwoStarredExpressions,
CheckCode::F631 => CheckKind::AssertTuple,
CheckCode::F632 => CheckKind::IsLiteral,
CheckCode::F633 => CheckKind::InvalidPrintSyntax,
CheckCode::F634 => CheckKind::IfTuple,
CheckCode::F701 => CheckKind::BreakOutsideLoop,
CheckCode::F702 => CheckKind::ContinueOutsideLoop,
CheckCode::F704 => CheckKind::YieldOutsideFunction,
CheckCode::F706 => CheckKind::ReturnOutsideFunction,
CheckCode::F707 => CheckKind::DefaultExceptNotLast,
CheckCode::F722 => CheckKind::ForwardAnnotationSyntaxError("...".to_string()),
CheckCode::F821 => CheckKind::UndefinedName("...".to_string()),
CheckCode::F822 => CheckKind::UndefinedExport("...".to_string()),
CheckCode::F823 => CheckKind::UndefinedLocal("...".to_string()),
CheckCode::F821 => CheckKind::UndefinedName("...".to_string()),
CheckCode::F401 => CheckKind::UnusedImport("...".to_string()),
CheckCode::F831 => CheckKind::DuplicateArgumentName,
CheckCode::F841 => CheckKind::UnusedVariable("...".to_string()),
CheckCode::F901 => CheckKind::RaiseNotImplemented,
CheckCode::M001 => CheckKind::UnusedNOQA(None),
CheckCode::R001 => CheckKind::UselessObjectInheritance("...".to_string()),
CheckCode::F704 => CheckKind::YieldOutsideFunction,
CheckCode::R002 => CheckKind::NoAssertEquals,
}
}
}
@@ -270,6 +328,7 @@ pub enum RejectedCmpop {
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CheckKind {
UnusedNOQA(Option<String>),
AmbiguousClassName(String),
AmbiguousFunctionName(String),
AmbiguousVariableName(String),
@@ -285,8 +344,10 @@ pub enum CheckKind {
FutureFeatureNotDefined(String),
IOError(String),
IfTuple,
ImportShadowedByLoopVar(String, usize),
ImportStarNotPermitted(String),
ImportStarUsage(String),
ImportStarUsage(String, String),
ImportStarUsed(String),
InvalidPrintSyntax,
IsLiteral,
LateFutureImport,
@@ -333,8 +394,10 @@ impl CheckKind {
CheckKind::FutureFeatureNotDefined(_) => "FutureFeatureNotDefined",
CheckKind::IOError(_) => "IOError",
CheckKind::IfTuple => "IfTuple",
CheckKind::ImportShadowedByLoopVar(_, _) => "ImportShadowedByLoopVar",
CheckKind::ImportStarNotPermitted(_) => "ImportStarNotPermitted",
CheckKind::ImportStarUsage(_) => "ImportStarUsage",
CheckKind::ImportStarUsage(_, _) => "ImportStarUsage",
CheckKind::ImportStarUsed(_) => "ImportStarUsed",
CheckKind::InvalidPrintSyntax => "InvalidPrintSyntax",
CheckKind::IsLiteral => "IsLiteral",
CheckKind::LateFutureImport => "LateFutureImport",
@@ -362,6 +425,7 @@ impl CheckKind {
CheckKind::UnusedVariable(_) => "UnusedVariable",
CheckKind::UselessObjectInheritance(_) => "UselessObjectInheritance",
CheckKind::YieldOutsideFunction => "YieldOutsideFunction",
CheckKind::UnusedNOQA(_) => "UnusedNOQA",
}
}
@@ -383,8 +447,10 @@ impl CheckKind {
CheckKind::FutureFeatureNotDefined(_) => &CheckCode::F407,
CheckKind::IOError(_) => &CheckCode::E902,
CheckKind::IfTuple => &CheckCode::F634,
CheckKind::ImportShadowedByLoopVar(_, _) => &CheckCode::F402,
CheckKind::ImportStarNotPermitted(_) => &CheckCode::F406,
CheckKind::ImportStarUsage(_) => &CheckCode::F403,
CheckKind::ImportStarUsage(_, _) => &CheckCode::F405,
CheckKind::ImportStarUsed(_) => &CheckCode::F403,
CheckKind::InvalidPrintSyntax => &CheckCode::F633,
CheckKind::IsLiteral => &CheckCode::F632,
CheckKind::LateFutureImport => &CheckCode::F404,
@@ -407,6 +473,7 @@ impl CheckKind {
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
CheckKind::UndefinedName(_) => &CheckCode::F821,
CheckKind::UnusedImport(_) => &CheckCode::F401,
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::UselessObjectInheritance(_) => &CheckCode::R001,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
@@ -449,17 +516,21 @@ impl CheckKind {
CheckKind::FutureFeatureNotDefined(name) => {
format!("future feature '{name}' is not defined")
}
CheckKind::IOError(name) => {
format!("No such file or directory: `{name}`")
}
CheckKind::IOError(message) => message.clone(),
CheckKind::IfTuple => "If test is a tuple, which is always `True`".to_string(),
CheckKind::InvalidPrintSyntax => "use of >> is invalid with print function".to_string(),
CheckKind::ImportShadowedByLoopVar(name, line) => {
format!("import '{name}' from line {line} shadowed by loop variable")
}
CheckKind::ImportStarNotPermitted(name) => {
format!("`from {name} import *` only allowed at module level")
}
CheckKind::ImportStarUsage(name) => {
CheckKind::ImportStarUsed(name) => {
format!("`from {name} import *` used; unable to detect undefined names")
}
CheckKind::ImportStarUsage(name, sources) => {
format!("'{name}' may be undefined, or defined from star imports: {sources}")
}
CheckKind::IsLiteral => "use ==/!= to compare constant literals".to_string(),
CheckKind::LateFutureImport => {
"from __future__ imports must occur at the beginning of the file".to_string()
@@ -536,6 +607,10 @@ impl CheckKind {
CheckKind::YieldOutsideFunction => {
"a `yield` or `yield from` statement outside of a function/method".to_string()
}
CheckKind::UnusedNOQA(code) => match code {
None => "Unused `noqa` directive".to_string(),
Some(code) => format!("Unused `noqa` directive for: {code}"),
},
}
}
@@ -543,7 +618,9 @@ impl CheckKind {
pub fn fixable(&self) -> bool {
matches!(
self,
CheckKind::NoAssertEquals | CheckKind::UselessObjectInheritance(_)
CheckKind::NoAssertEquals
| CheckKind::UselessObjectInheritance(_)
| CheckKind::UnusedNOQA(_)
)
}
}
@@ -563,11 +640,6 @@ pub struct Check {
pub fix: Option<Fix>,
}
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?").expect("Invalid regex")
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").expect("Invalid regex"));
impl Check {
pub fn new(kind: CheckKind, location: Location) -> Self {
Self {
@@ -580,25 +652,4 @@ impl Check {
pub fn amend(&mut self, fix: Fix) {
self.fix = Some(fix);
}
pub fn is_inline_ignored(&self, line: &str) -> bool {
match NO_QA_REGEX.captures(line) {
Some(caps) => match caps.name("codes") {
Some(codes) => {
for code in SPLIT_COMMA_REGEX
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())
{
if code == self.kind.code().as_str() {
return true;
}
}
false
}
None => true,
},
None => false,
}
}
}

View File

@@ -59,14 +59,14 @@ pub fn iter_python_files<'a>(
path: &'a Path,
exclude: &'a [FilePattern],
extend_exclude: &'a [FilePattern],
) -> impl Iterator<Item = DirEntry> + 'a {
) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> + 'a {
// Run some checks over the provided patterns, to enable optimizations below.
let has_exclude = !exclude.is_empty();
let has_extend_exclude = !extend_exclude.is_empty();
let exclude_simple = exclude
.iter()
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
let extend_exclude_simple = exclude
let extend_exclude_simple = extend_exclude
.iter()
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
@@ -105,18 +105,15 @@ pub fn iter_python_files<'a>(
}
}
})
.filter_map(|entry| entry.ok())
.filter(|entry| {
let path = entry.path();
is_included(path)
entry.as_ref().map_or(true, |entry| {
(entry.depth() == 0 && !entry.file_type().is_dir()) || is_included(entry.path())
})
})
}
/// Convert any path to an absolute path (based on the current working directory).
pub fn normalize_path(path: &Path) -> PathBuf {
if path == Path::new(".") || path == Path::new("..") {
return path.to_path_buf();
}
if let Ok(path) = path.absolutize() {
return path.to_path_buf();
}
@@ -125,9 +122,6 @@ pub fn normalize_path(path: &Path) -> PathBuf {
/// Convert any path to an absolute path (based on the specified project root).
pub fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
if path == Path::new(".") || path == Path::new("..") {
return path.to_path_buf();
}
if let Ok(path) = path.absolutize_from(project_root) {
return path.to_path_buf();
}

View File

@@ -10,6 +10,7 @@ pub mod fs;
pub mod linter;
pub mod logging;
pub mod message;
mod noqa;
pub mod printer;
pub mod pyproject;
mod python;

View File

@@ -2,40 +2,57 @@ use std::path::Path;
use anyhow::Result;
use log::debug;
use rustpython_parser::parser;
use rustpython_parser::lexer::LexResult;
use rustpython_parser::{lexer, parser};
use crate::autofix::fixer;
use crate::autofix::fixer::fix_file;
use crate::check_ast::check_ast;
use crate::check_lines::check_lines;
use crate::checks::{Check, LintSource};
use crate::checks::{Check, CheckCode, CheckKind, LintSource};
use crate::message::Message;
use crate::noqa::add_noqa;
use crate::settings::Settings;
use crate::{cache, fs};
use crate::{cache, fs, noqa};
fn check_path(
path: &Path,
contents: &str,
tokens: Vec<LexResult>,
settings: &Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
) -> Vec<Check> {
// Aggregate all checks.
let mut checks: Vec<Check> = vec![];
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Run the AST-based checks.
if settings
.select
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::AST))
{
let python_ast = parser::parse_program(contents, "<filename>")?;
checks.extend(check_ast(&python_ast, contents, settings, autofix, path));
match parser::parse_program_tokens(tokens, "<filename>") {
Ok(python_ast) => {
checks.extend(check_ast(&python_ast, contents, settings, autofix, path))
}
Err(parse_error) => {
if settings.select.contains(&CheckCode::E999) {
checks.push(Check::new(
CheckKind::SyntaxError(parse_error.error.to_string()),
parse_error.location,
))
}
}
}
}
// Run the lines-based checks.
check_lines(&mut checks, contents, settings);
check_lines(&mut checks, contents, &noqa_line_for, settings, autofix);
Ok(checks)
checks
}
pub fn lint_path(
@@ -55,8 +72,11 @@ pub fn lint_path(
// Read the file from disk.
let contents = fs::read_file(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = lexer::make_tokenizer(&contents).collect();
// Generate checks.
let mut checks = check_path(path, &contents, settings, autofix)?;
let mut checks = check_path(path, &contents, tokens, settings, autofix);
// Apply autofix.
if matches!(autofix, fixer::Mode::Apply) {
@@ -78,12 +98,29 @@ pub fn lint_path(
Ok(messages)
}
pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
// Read the file from disk.
let contents = fs::read_file(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = lexer::make_tokenizer(&contents).collect();
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Generate checks.
let checks = check_path(path, &contents, tokens, settings, &fixer::Mode::None);
add_noqa(&checks, &contents, &noqa_line_for, path)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::path::Path;
use anyhow::Result;
use rustpython_parser::lexer;
use rustpython_parser::lexer::LexResult;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
@@ -97,19 +134,17 @@ mod tests {
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
linter::check_path(path, &contents, settings, autofix)
let tokens: Vec<LexResult> = lexer::make_tokenizer(&contents).collect();
Ok(linter::check_path(
path, &contents, tokens, settings, autofix,
))
}
#[test]
fn e402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E402.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E402]),
},
&settings::Settings::for_rule(CheckCode::E402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -121,12 +156,7 @@ mod tests {
fn e501() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E501.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E501]),
},
&settings::Settings::for_rule(CheckCode::E501),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -138,12 +168,7 @@ mod tests {
fn e711() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E711.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E711]),
},
&settings::Settings::for_rule(CheckCode::E711),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -155,12 +180,7 @@ mod tests {
fn e712() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E712.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E712]),
},
&settings::Settings::for_rule(CheckCode::E712),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -172,12 +192,7 @@ mod tests {
fn e713() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E713.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E713]),
},
&settings::Settings::for_rule(CheckCode::E713),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -189,12 +204,7 @@ mod tests {
fn e721() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E721.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E721]),
},
&settings::Settings::for_rule(CheckCode::E721),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -206,12 +216,7 @@ mod tests {
fn e722() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E722.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E722]),
},
&settings::Settings::for_rule(CheckCode::E722),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -223,12 +228,7 @@ mod tests {
fn e714() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E714.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E714]),
},
&settings::Settings::for_rule(CheckCode::E714),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -240,12 +240,7 @@ mod tests {
fn e731() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E731.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E731]),
},
&settings::Settings::for_rule(CheckCode::E731),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -257,12 +252,7 @@ mod tests {
fn e741() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E741.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E741]),
},
&settings::Settings::for_rule(CheckCode::E741),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -274,12 +264,7 @@ mod tests {
fn e742() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E742.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E742]),
},
&settings::Settings::for_rule(CheckCode::E742),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -291,12 +276,7 @@ mod tests {
fn e743() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E743.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E743]),
},
&settings::Settings::for_rule(CheckCode::E743),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -308,12 +288,19 @@ mod tests {
fn f401() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F401.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F401]),
},
&settings::Settings::for_rule(CheckCode::F401),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F402.py"),
&settings::Settings::for_rule(CheckCode::F402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -325,12 +312,7 @@ mod tests {
fn f403() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F403.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F403]),
},
&settings::Settings::for_rule(CheckCode::F403),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -342,12 +324,19 @@ mod tests {
fn f404() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F404.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F404]),
},
&settings::Settings::for_rule(CheckCode::F404),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f405() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F405.py"),
&settings::Settings::for_rule(CheckCode::F405),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -359,12 +348,7 @@ mod tests {
fn f406() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F406.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F406]),
},
&settings::Settings::for_rule(CheckCode::F406),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -376,12 +360,7 @@ mod tests {
fn f407() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F407.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F407]),
},
&settings::Settings::for_rule(CheckCode::F407),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -393,12 +372,7 @@ mod tests {
fn f541() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F541.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F541]),
},
&settings::Settings::for_rule(CheckCode::F541),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -410,12 +384,7 @@ mod tests {
fn f601() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F601.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F601]),
},
&settings::Settings::for_rule(CheckCode::F601),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -427,12 +396,7 @@ mod tests {
fn f602() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F602.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F602]),
},
&settings::Settings::for_rule(CheckCode::F602),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -444,12 +408,7 @@ mod tests {
fn f622() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F622.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F622]),
},
&settings::Settings::for_rule(CheckCode::F622),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -461,12 +420,7 @@ mod tests {
fn f631() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F631.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F631]),
},
&settings::Settings::for_rule(CheckCode::F631),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -478,12 +432,7 @@ mod tests {
fn f632() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F632.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F632]),
},
&settings::Settings::for_rule(CheckCode::F632),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -495,12 +444,7 @@ mod tests {
fn f633() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F633.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F633]),
},
&settings::Settings::for_rule(CheckCode::F633),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -512,12 +456,7 @@ mod tests {
fn f634() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F634.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F634]),
},
&settings::Settings::for_rule(CheckCode::F634),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -529,12 +468,7 @@ mod tests {
fn f701() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F701.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F701]),
},
&settings::Settings::for_rule(CheckCode::F701),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -546,12 +480,7 @@ mod tests {
fn f702() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F702.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F702]),
},
&settings::Settings::for_rule(CheckCode::F702),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -563,12 +492,7 @@ mod tests {
fn f704() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F704.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F704]),
},
&settings::Settings::for_rule(CheckCode::F704),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -580,12 +504,7 @@ mod tests {
fn f706() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F706.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F706]),
},
&settings::Settings::for_rule(CheckCode::F706),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -597,12 +516,7 @@ mod tests {
fn f707() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F707.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F707]),
},
&settings::Settings::for_rule(CheckCode::F707),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -614,12 +528,7 @@ mod tests {
fn f722() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F722.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F722]),
},
&settings::Settings::for_rule(CheckCode::F722),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -631,12 +540,7 @@ mod tests {
fn f821() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F821.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F821]),
},
&settings::Settings::for_rule(CheckCode::F821),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -648,12 +552,7 @@ mod tests {
fn f822() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F822.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F822]),
},
&settings::Settings::for_rule(CheckCode::F822),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -665,12 +564,7 @@ mod tests {
fn f823() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F823.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F823]),
},
&settings::Settings::for_rule(CheckCode::F823),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -682,12 +576,7 @@ mod tests {
fn f831() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F831.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F831]),
},
&settings::Settings::for_rule(CheckCode::F831),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -699,12 +588,7 @@ mod tests {
fn f841() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F841.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F841]),
},
&settings::Settings::for_rule(CheckCode::F841),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -716,12 +600,19 @@ mod tests {
fn f901() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F901.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F901]),
},
&settings::Settings::for_rule(CheckCode::F901),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn m001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/M001.py"),
&settings::Settings::for_rules(vec![CheckCode::M001, CheckCode::E501, CheckCode::F841]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -733,12 +624,7 @@ mod tests {
fn r001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/R001.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::R001]),
},
&settings::Settings::for_rule(CheckCode::R001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -750,12 +636,7 @@ mod tests {
fn r002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/R002.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::R002]),
},
&settings::Settings::for_rule(CheckCode::R002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -767,12 +648,7 @@ mod tests {
fn init() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/__init__.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F821, CheckCode::F822]),
},
&settings::Settings::for_rules(vec![CheckCode::F821, CheckCode::F822]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -784,12 +660,7 @@ mod tests {
fn future_annotations() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/future_annotations.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F401, CheckCode::F821]),
},
&settings::Settings::for_rules(vec![CheckCode::F401, CheckCode::F821]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);

View File

@@ -1,6 +1,7 @@
extern crate core;
use std::path::PathBuf;
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::Instant;
@@ -24,6 +25,7 @@ use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::pyproject;
use ::ruff::settings::{FilePattern, Settings};
use ::ruff::tell_user;
use ruff::linter::add_noqa_to_path;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -56,9 +58,15 @@ struct Cli {
/// List of error codes to enable.
#[clap(long, multiple = true)]
select: Vec<CheckCode>,
/// Like --select, but adds additional error codes on top of the selected ones.
#[clap(long, multiple = true)]
extend_select: Vec<CheckCode>,
/// List of error codes to ignore.
#[clap(long, multiple = true)]
ignore: Vec<CheckCode>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[clap(long, multiple = true)]
extend_ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[clap(long, multiple = true)]
exclude: Vec<String>,
@@ -68,6 +76,15 @@ struct Cli {
/// Output serialization format for error messages.
#[clap(long, arg_enum, default_value_t=SerializationFormat::Text)]
format: SerializationFormat,
/// See the files ruff will be run against with the current settings.
#[clap(long, action)]
show_files: bool,
/// See ruff's settings.
#[clap(long, action)]
show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
#[clap(long, action)]
add_noqa: bool,
}
#[cfg(feature = "update-informer")]
@@ -94,6 +111,22 @@ fn check_for_updates() {
}
}
fn show_settings(settings: &Settings) {
println!("{:#?}", settings);
}
fn show_files(files: &[PathBuf], settings: &Settings) {
let mut entries: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flatten()
.collect();
entries.sort_by(|a, b| a.path().cmp(b.path()));
for entry in entries {
println!("{}", entry.path().to_string_lossy());
}
}
fn run_once(
files: &[PathBuf],
settings: &Settings,
@@ -102,7 +135,7 @@ fn run_once(
) -> Result<Vec<Message>> {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<DirEntry> = files
let paths: Vec<Result<DirEntry, walkdir::Error>> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.collect();
@@ -113,16 +146,33 @@ fn run_once(
let mut messages: Vec<Message> = paths
.par_iter()
.map(|entry| {
lint_path(entry.path(), settings, &cache.into(), &autofix.into()).unwrap_or_else(|e| {
if settings.select.contains(&CheckCode::E999) {
vec![Message {
kind: CheckKind::SyntaxError(e.to_string()),
fixed: false,
location: Default::default(),
filename: entry.path().to_string_lossy().to_string(),
}]
match entry {
Ok(entry) => {
let path = entry.path();
lint_path(path, settings, &cache.into(), &autofix.into())
.map_err(|e| (Some(path.to_owned()), e.to_string()))
}
Err(e) => Err((
e.path().map(Path::to_owned),
e.io_error()
.map_or_else(|| e.to_string(), io::Error::to_string),
)),
}
.unwrap_or_else(|(path, message)| {
if let Some(path) = path {
if settings.select.contains(&CheckCode::E902) {
vec![Message {
kind: CheckKind::IOError(message),
fixed: false,
location: Default::default(),
filename: path.to_string_lossy().to_string(),
}]
} else {
error!("Failed to check {}: {message}", path.to_string_lossy());
vec![]
}
} else {
error!("Failed to check {}: {e:?}", entry.path().to_string_lossy());
error!("{message}");
vec![]
}
})
@@ -130,19 +180,6 @@ fn run_once(
.flatten()
.collect();
if settings.select.contains(&CheckCode::E902) {
for file in files {
if !file.exists() {
messages.push(Message {
kind: CheckKind::IOError(file.to_string_lossy().to_string()),
fixed: false,
location: Default::default(),
filename: file.to_string_lossy().to_string(),
})
}
}
}
messages.sort_unstable();
let duration = start.elapsed();
debug!("Checked files in: {:?}", duration);
@@ -150,6 +187,35 @@ fn run_once(
Ok(messages)
}
fn add_noqa(files: &[PathBuf], settings: &Settings) -> Result<usize> {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<Result<DirEntry, walkdir::Error>> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications: usize = paths
.par_iter()
.map(|entry| match entry {
Ok(entry) => {
let path = entry.path();
add_noqa_to_path(path, settings)
}
Err(_) => Ok(0),
})
.flatten()
.sum();
let duration = start.elapsed();
debug!("Added noqa to files in: {:?}", duration);
Ok(modifications)
}
fn inner_main() -> Result<ExitCode> {
let cli = Cli::parse();
@@ -168,36 +234,63 @@ fn inner_main() -> Result<ExitCode> {
};
// Parse the settings from the pyproject.toml and command-line arguments.
let mut settings = Settings::from_pyproject(&pyproject, &project_root);
let exclude: Vec<FilePattern> = cli
.exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
let extend_exclude: Vec<FilePattern> = cli
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
let mut settings = Settings::from_pyproject(pyproject, project_root);
if !exclude.is_empty() {
settings.exclude = exclude;
}
if !extend_exclude.is_empty() {
settings.extend_exclude = extend_exclude;
}
if !cli.select.is_empty() {
settings.clear();
settings.select(cli.select);
}
if !cli.extend_select.is_empty() {
settings.select(cli.extend_select);
}
if !cli.ignore.is_empty() {
settings.ignore(&cli.ignore);
}
if !cli.exclude.is_empty() {
settings.exclude = cli
.exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
if !cli.extend_ignore.is_empty() {
settings.ignore(&cli.extend_ignore);
}
if !cli.extend_exclude.is_empty() {
settings.extend_exclude = cli
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
if cli.show_settings && cli.show_files {
println!("Error: specify --show-settings or show-files (not both).");
return Ok(ExitCode::FAILURE);
}
if cli.show_settings {
show_settings(&settings);
return Ok(ExitCode::SUCCESS);
}
if cli.show_files {
show_files(&cli.files, &settings);
return Ok(ExitCode::SUCCESS);
}
cache::init()?;
let mut printer = Printer::new(cli.format);
let mut printer = Printer::new(cli.format, cli.verbose);
if cli.watch {
if cli.fix {
println!("Warning: --fix is not enabled in watch mode.");
}
if cli.add_noqa {
println!("Warning: --no-qa is not enabled in watch mode.");
}
if cli.format != SerializationFormat::Text {
println!("Warning: --format 'text' is used in watch mode.");
}
@@ -236,6 +329,11 @@ fn inner_main() -> Result<ExitCode> {
Err(e) => return Err(e.into()),
}
}
} else if cli.add_noqa {
let modifications = add_noqa(&cli.files, &settings)?;
if modifications > 0 {
println!("Added {modifications} noqa directives.");
}
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
if !cli.quiet {

280
src/noqa.rs Normal file
View File

@@ -0,0 +1,280 @@
use std::cmp::{max, min};
use crate::checks::{Check, CheckCode};
use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::lexer::{LexResult, Tok};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)(?P<noqa>\s*# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)")
.expect("Invalid regex")
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").expect("Invalid regex"));
#[derive(Debug)]
pub enum Directive<'a> {
None,
All(usize),
Codes(usize, Vec<&'a str>),
}
pub fn extract_noqa_directive(line: &str) -> Directive {
match NO_QA_REGEX.captures(line) {
Some(caps) => match caps.name("noqa") {
Some(noqa) => match caps.name("codes") {
Some(codes) => Directive::Codes(
noqa.start(),
SPLIT_COMMA_REGEX
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())
.collect(),
),
None => Directive::All(noqa.start()),
},
None => Directive::None,
},
None => Directive::None,
}
}
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> Vec<usize> {
let mut noqa_line_for: Vec<usize> = vec![];
let mut last_is_string = false;
let mut last_seen = usize::MIN;
let mut min_line = usize::MAX;
let mut max_line = usize::MIN;
for (start, tok, end) in lxr.iter().flatten() {
if matches!(tok, Tok::EndOfFile) {
break;
}
if matches!(tok, Tok::Newline) {
min_line = min(min_line, start.row());
max_line = max(max_line, start.row());
// For now, we only care about preserving noqa directives across multi-line strings.
if last_is_string {
noqa_line_for.extend(vec![max_line; (max_line + 1) - min_line]);
} else {
for i in (min_line - 1)..(max_line) {
noqa_line_for.push(i + 1);
}
}
min_line = usize::MAX;
max_line = usize::MIN;
} else {
// Handle empty lines.
if start.row() > last_seen {
for i in last_seen..(start.row() - 1) {
noqa_line_for.push(i + 1);
}
}
min_line = min(min_line, start.row());
max_line = max(max_line, end.row());
}
last_seen = start.row();
last_is_string = matches!(tok, Tok::String { .. });
}
noqa_line_for
}
fn add_noqa_inner(
checks: &Vec<Check>,
contents: &str,
noqa_line_for: &[usize],
) -> Result<(usize, String)> {
let lines: Vec<&str> = contents.lines().collect();
let mut matches_by_line: BTreeMap<usize, BTreeSet<&CheckCode>> = BTreeMap::new();
for lineno in 0..lines.len() {
let mut codes: BTreeSet<&CheckCode> = BTreeSet::new();
for check in checks {
if check.location.row() == lineno + 1 {
codes.insert(check.kind.code());
}
}
// 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)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
if !codes.is_empty() {
let matches = matches_by_line
.entry(noqa_lineno)
.or_insert_with(BTreeSet::new);
matches.append(&mut codes);
}
}
let mut count: usize = 0;
let mut output = "".to_string();
for (lineno, line) in lines.iter().enumerate() {
match matches_by_line.get(&lineno) {
None => {
output.push_str(line);
output.push('\n');
}
Some(codes) => {
match extract_noqa_directive(line) {
Directive::None => {
output.push_str(line);
}
Directive::All(start) => output.push_str(&line[..start]),
Directive::Codes(start, _) => output.push_str(&line[..start]),
};
let codes: Vec<&str> = codes.iter().map(|code| code.as_str()).collect();
output.push_str(" # noqa: ");
output.push_str(&codes.join(", "));
output.push('\n');
count += 1;
}
}
}
Ok((count, output))
}
pub fn add_noqa(
checks: &Vec<Check>,
contents: &str,
noqa_line_for: &[usize],
path: &Path,
) -> Result<usize> {
let (count, output) = add_noqa_inner(checks, contents, noqa_line_for)?;
fs::write(path, output)?;
Ok(count)
}
#[cfg(test)]
mod tests {
use crate::checks::{Check, CheckKind};
use anyhow::Result;
use rustpython_parser::ast::Location;
use rustpython_parser::lexer;
use rustpython_parser::lexer::LexResult;
use crate::noqa::{add_noqa_inner, extract_noqa_line_for};
#[test]
fn extraction() -> Result<()> {
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 2, 3]);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"
x = 1
y = 2
z = x + 1",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 2, 3, 4]);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1
",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 2, 3]);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1
",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 2, 3, 4]);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = '''abc
def
ghi
'''
y = 2
z = x + 1",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), vec![4, 4, 4, 4, 5, 6]);
Ok(())
}
#[test]
fn modification() -> Result<()> {
let checks = vec![];
let contents = "x = 1";
let noqa_line_for = vec![1];
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 0);
assert_eq!(output.trim(), contents.trim());
let checks = vec![Check::new(
CheckKind::UnusedVariable("x".to_string()),
Location::new(1, 1),
)];
let contents = "x = 1";
let noqa_line_for = vec![1];
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: F841".trim());
let checks = vec![
Check::new(
CheckKind::AmbiguousVariableName("x".to_string()),
Location::new(1, 1),
),
Check::new(
CheckKind::UnusedVariable("x".to_string()),
Location::new(1, 1),
),
];
let contents = "x = 1 # noqa: E741";
let noqa_line_for = vec![1];
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: E741, F841".trim());
let checks = vec![
Check::new(
CheckKind::AmbiguousVariableName("x".to_string()),
Location::new(1, 1),
),
Check::new(
CheckKind::UnusedVariable("x".to_string()),
Location::new(1, 1),
),
];
let contents = "x = 1 # noqa";
let noqa_line_for = vec![1];
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: E741, F841".trim());
Ok(())
}
}

View File

@@ -1,7 +1,6 @@
use colored::Colorize;
use anyhow::Result;
use clap::ValueEnum;
use colored::Colorize;
use crate::message::Message;
use crate::tell_user;
@@ -14,11 +13,12 @@ pub enum SerializationFormat {
pub struct Printer {
format: SerializationFormat,
verbose: bool,
}
impl Printer {
pub fn new(format: SerializationFormat) -> Self {
Self { format }
pub fn new(format: SerializationFormat, verbose: bool) -> Self {
Self { format, verbose }
}
pub fn write_once(&mut self, messages: &[Message]) -> Result<()> {
@@ -40,7 +40,7 @@ impl Printer {
outstanding.len(),
fixed.len()
)
} else {
} else if !outstanding.is_empty() || self.verbose {
println!("Found {} error(s).", outstanding.len())
}

View File

@@ -1,9 +1,8 @@
use std::ops::Deref;
use std::path::{Path, PathBuf};
use anyhow::Result;
use common_path::common_path_all;
use path_absolutize::path_dedot;
use path_absolutize::Absolutize;
use serde::Deserialize;
use crate::checks::CheckCode;
@@ -78,8 +77,10 @@ fn find_user_pyproject_toml() -> Option<PathBuf> {
}
pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
let cwd = path_dedot::CWD.deref();
let absolute_sources: Vec<PathBuf> = sources.iter().map(|source| cwd.join(source)).collect();
let absolute_sources: Vec<PathBuf> = sources
.iter()
.flat_map(|source| source.absolutize().map(|path| path.to_path_buf()))
.collect();
if let Some(prefix) = common_path_all(absolute_sources.iter().map(PathBuf::as_path)) {
for directory in prefix.ancestors() {
if directory.join(".git").is_dir() {

View File

@@ -1,6 +1,7 @@
use once_cell::sync::Lazy;
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
static ANNOTATED_SUBSCRIPTS: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"AbstractAsyncContextManager",

View File

@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use glob::Pattern;
use once_cell::sync::Lazy;
use crate::checks::{CheckCode, ALL_CHECK_CODES};
use crate::checks::{CheckCode, DEFAULT_CHECK_CODES};
use crate::fs;
use crate::pyproject::load_config;
@@ -36,12 +36,38 @@ impl FilePattern {
#[derive(Debug)]
pub struct Settings {
pub pyproject: Option<PathBuf>,
pub project_root: Option<PathBuf>,
pub line_length: usize,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub select: BTreeSet<CheckCode>,
}
impl Settings {
pub fn for_rule(check_code: CheckCode) -> Self {
Self {
pyproject: None,
project_root: None,
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([check_code]),
}
}
pub fn for_rules(check_codes: Vec<CheckCode>) -> Self {
Self {
pyproject: None,
project_root: None,
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from_iter(check_codes),
}
}
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
self.line_length.hash(state);
@@ -76,8 +102,8 @@ static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
});
impl Settings {
pub fn from_pyproject(path: &Option<PathBuf>, project_root: &Option<PathBuf>) -> Self {
let config = load_config(path);
pub fn from_pyproject(pyproject: Option<PathBuf>, project_root: Option<PathBuf>) -> Self {
let config = load_config(&pyproject);
let mut settings = Settings {
line_length: config.line_length.unwrap_or(88),
exclude: config
@@ -85,7 +111,7 @@ impl Settings {
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.map(|path| FilePattern::from_user(path, &project_root))
.collect()
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
@@ -94,15 +120,17 @@ impl Settings {
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.map(|path| FilePattern::from_user(path, &project_root))
.collect()
})
.unwrap_or_default(),
select: if let Some(select) = config.select {
BTreeSet::from_iter(select)
} else {
BTreeSet::from_iter(ALL_CHECK_CODES)
BTreeSet::from_iter(DEFAULT_CHECK_CODES)
},
pyproject,
project_root,
};
if let Some(ignore) = &config.ignore {
settings.ignore(ignore);
@@ -110,8 +138,11 @@ impl Settings {
settings
}
pub fn select(&mut self, codes: Vec<CheckCode>) {
pub fn clear(&mut self) {
self.select.clear();
}
pub fn select(&mut self, codes: Vec<CheckCode>) {
for code in codes {
self.select.insert(code);
}

View File

@@ -0,0 +1,21 @@
---
source: src/linter.rs
expression: checks
---
- kind:
ImportShadowedByLoopVar:
- os
- 1
location:
row: 5
column: 5
fix: ~
- kind:
ImportShadowedByLoopVar:
- path
- 2
location:
row: 8
column: 5
fix: ~

View File

@@ -3,13 +3,13 @@ source: src/linter.rs
expression: checks
---
- kind:
ImportStarUsage: F634
ImportStarUsed: F634
location:
row: 1
column: 1
fix: ~
- kind:
ImportStarUsage: F634
ImportStarUsed: F634
location:
row: 2
column: 1

View File

@@ -0,0 +1,21 @@
---
source: src/linter.rs
expression: checks
---
- kind:
ImportStarUsage:
- name
- mymodule
location:
row: 5
column: 11
fix: ~
- kind:
ImportStarUsage:
- a
- mymodule
location:
row: 11
column: 1
fix: ~

View File

@@ -38,4 +38,16 @@ expression: checks
row: 83
column: 11
fix: ~
- kind:
UndefinedName: B
location:
row: 87
column: 7
fix: ~
- kind:
UndefinedName: B
location:
row: 89
column: 7
fix: ~

View File

@@ -0,0 +1,89 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnusedNOQA: ~
location:
row: 9
column: 10
fix:
content: ""
start:
row: 9
column: 10
end:
row: 9
column: 18
applied: false
- kind:
UnusedNOQA: E501
location:
row: 13
column: 10
fix:
content: ""
start:
row: 13
column: 10
end:
row: 13
column: 24
applied: false
- kind:
UnusedNOQA: E501
location:
row: 16
column: 10
fix:
content: " # noqa: F841"
start:
row: 16
column: 10
end:
row: 16
column: 30
applied: false
- kind:
UnusedNOQA: F841
location:
row: 41
column: 4
fix:
content: " # noqa: E501"
start:
row: 41
column: 4
end:
row: 41
column: 24
applied: false
- kind:
UnusedNOQA: E501
location:
row: 49
column: 4
fix:
content: ""
start:
row: 49
column: 4
end:
row: 49
column: 18
applied: false
- kind:
UnusedNOQA: ~
location:
row: 57
column: 4
fix:
content: ""
start:
row: 57
column: 4
end:
row: 57
column: 12
applied: false