Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36fcfad56a | ||
|
|
65d29d9734 | ||
|
|
1e171ce0e8 | ||
|
|
2bdc500c61 | ||
|
|
f453e429b6 | ||
|
|
73874f4788 | ||
|
|
8846dcdf6a | ||
|
|
d827e6e36a | ||
|
|
71d9b2ac5f | ||
|
|
401b53cc45 | ||
|
|
aa9c1e255c | ||
|
|
f7fc702b2c | ||
|
|
50ca0d7d0a | ||
|
|
65e0284698 | ||
|
|
e4f571ea61 | ||
|
|
4ed88dd245 | ||
|
|
09b926fd59 | ||
|
|
a4869e4974 | ||
|
|
f53c4fc221 | ||
|
|
3892a49a97 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1801,7 +1801,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.40"
|
||||
version = "0.0.44"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.40"
|
||||
version = "0.0.44"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
172
README.md
172
README.md
@@ -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.44)
|
||||
An extremely fast Python linter.
|
||||
|
||||
USAGE:
|
||||
@@ -138,32 +136,25 @@ Exclusions are based on globs, and can be either:
|
||||
|
||||
### 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 +173,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 |
|
||||
@@ -247,29 +240,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 +271,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 +297,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 +308,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
|
||||
|
||||
9
resources/test/fixtures/F402.py
vendored
Normal file
9
resources/test/fixtures/F402.py
vendored
Normal 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
11
resources/test/fixtures/F405.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
from mymodule import *
|
||||
|
||||
|
||||
def print_name():
|
||||
print(name)
|
||||
|
||||
|
||||
def print_name(name):
|
||||
print(name)
|
||||
|
||||
__all__ = ['a']
|
||||
7
resources/test/fixtures/F821.py
vendored
7
resources/test/fixtures/F821.py
vendored
@@ -82,3 +82,10 @@ class Ticket:
|
||||
def update_tomato():
|
||||
print(TOMATO)
|
||||
TOMATO = "cherry tomato"
|
||||
|
||||
|
||||
A = f'{B}'
|
||||
A = (
|
||||
f'B'
|
||||
f'{B}'
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
288
src/check_ast.rs
288
src/check_ast.rs
@@ -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),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -56,9 +56,10 @@ 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() {
|
||||
|
||||
102
src/checks.rs
102
src/checks.rs
@@ -6,7 +6,7 @@ use regex::Regex;
|
||||
use rustpython_parser::ast::Location;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const ALL_CHECK_CODES: [CheckCode; 42] = [
|
||||
pub const ALL_CHECK_CODES: [CheckCode; 44] = [
|
||||
CheckCode::E402,
|
||||
CheckCode::E501,
|
||||
CheckCode::E711,
|
||||
@@ -22,8 +22,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,
|
||||
@@ -68,8 +70,10 @@ pub enum CheckCode {
|
||||
E902,
|
||||
E999,
|
||||
F401,
|
||||
F402,
|
||||
F403,
|
||||
F404,
|
||||
F405,
|
||||
F406,
|
||||
F407,
|
||||
F541,
|
||||
@@ -117,8 +121,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),
|
||||
@@ -167,8 +173,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",
|
||||
@@ -209,48 +217,50 @@ 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::R001 => CheckKind::UselessObjectInheritance("...".to_string()),
|
||||
CheckCode::F704 => CheckKind::YieldOutsideFunction,
|
||||
CheckCode::R002 => CheckKind::NoAssertEquals,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,8 +295,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 +345,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",
|
||||
@@ -383,8 +397,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::ImportStarUsed(_) => &CheckCode::F403,
|
||||
CheckKind::ImportStarUsage(_, _) => &CheckCode::F405,
|
||||
CheckKind::InvalidPrintSyntax => &CheckCode::F633,
|
||||
CheckKind::IsLiteral => &CheckCode::F632,
|
||||
CheckKind::LateFutureImport => &CheckCode::F404,
|
||||
@@ -449,17 +465,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()
|
||||
|
||||
16
src/fs.rs
16
src/fs.rs
@@ -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();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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::settings::Settings;
|
||||
use crate::{cache, fs};
|
||||
@@ -18,7 +18,7 @@ fn check_path(
|
||||
contents: &str,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Result<Vec<Check>> {
|
||||
) -> Vec<Check> {
|
||||
// Aggregate all checks.
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
@@ -28,14 +28,25 @@ fn check_path(
|
||||
.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(contents, "<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);
|
||||
|
||||
Ok(checks)
|
||||
checks
|
||||
}
|
||||
|
||||
pub fn lint_path(
|
||||
@@ -56,7 +67,7 @@ pub fn lint_path(
|
||||
let contents = fs::read_file(path)?;
|
||||
|
||||
// Generate checks.
|
||||
let mut checks = check_path(path, &contents, settings, autofix)?;
|
||||
let mut checks = check_path(path, &contents, settings, autofix);
|
||||
|
||||
// Apply autofix.
|
||||
if matches!(autofix, fixer::Mode::Apply) {
|
||||
@@ -97,7 +108,7 @@ mod tests {
|
||||
autofix: &fixer::Mode,
|
||||
) -> Result<Vec<Check>> {
|
||||
let contents = fs::read_file(path)?;
|
||||
linter::check_path(path, &contents, settings, autofix)
|
||||
Ok(linter::check_path(path, &contents, settings, autofix))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -321,6 +332,23 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f402() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F402.py"),
|
||||
&settings::Settings {
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
extend_exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F402]),
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f403() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
@@ -355,6 +383,23 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f405() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F405.py"),
|
||||
&settings::Settings {
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
extend_exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F405]),
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f406() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
|
||||
55
src/main.rs
55
src/main.rs
@@ -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;
|
||||
@@ -102,7 +103,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 +114,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 +148,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);
|
||||
@@ -192,7 +197,7 @@ fn inner_main() -> Result<ExitCode> {
|
||||
|
||||
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.");
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
21
src/snapshots/ruff__linter__tests__f402.snap
Normal file
21
src/snapshots/ruff__linter__tests__f402.snap
Normal 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: ~
|
||||
|
||||
@@ -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
|
||||
|
||||
21
src/snapshots/ruff__linter__tests__f405.snap
Normal file
21
src/snapshots/ruff__linter__tests__f405.snap
Normal 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: ~
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
|
||||
Reference in New Issue
Block a user