Compare commits

...

36 Commits

Author SHA1 Message Date
Charlie Marsh
a716cd1989 Compile for WASM
- wasm-pack build --target web
- rm -rf demo/src/pkg && cp -r pkg demo/src/pkg
- npm run dev
2022-09-21 18:28:50 -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
Charlie Marsh
27cc7e236c Use a separate repo for pre-commit (#229) 2022-09-19 21:06:39 -06:00
Charlie Marsh
fa0954fe47 Treat relative excludes as relative to project root (#228) 2022-09-19 20:45:02 -06:00
Charlie Marsh
a0b50d7ebc Use absolute paths for exclusion matching (#213) 2022-09-19 20:32:31 -06:00
Charlie Marsh
afe7a04211 Ignore F841 violations when locals() is in scope (#226) 2022-09-19 20:13:55 -06:00
Charlie Marsh
14806c62ca Reduce number of sites for new check definitions (#227) 2022-09-19 20:13:46 -06:00
Suguru Yamamoto
0d0c8730fa fix: Use UTF-32 char count for line length (#223) (#224) 2022-09-18 10:45:41 -06:00
Harutaka Kawamura
cf6a23b83c Add --version flag (#222) 2022-09-18 09:15:15 -06:00
Anders Kaseorg
9e0daac561 Enable F404 by default (#219) 2022-09-18 09:13:23 -06:00
Anders Kaseorg
f2fd7335ce Use a platform-appropriate location for user configuration (#215) 2022-09-17 13:29:17 -06:00
Anders Kaseorg
b8f878df5e Find user configuration even if there’s no project directory (#216) 2022-09-16 21:47:15 -06:00
Anders Kaseorg
9bdb922c75 Detect multi-target assignment as unpacking if *any* target is unpacking (#217) 2022-09-16 21:45:44 -06:00
Anders Kaseorg
edecc1bba6 Fix find_project_root with relative paths (#214) 2022-09-16 18:04:35 -06:00
Charlie Marsh
8e903153f6 Update README to include more badges 2022-09-16 12:18:03 -06:00
51 changed files with 3623 additions and 4062 deletions

View File

@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/charliermarsh/ruff
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.40
hooks:
- id: lint

View File

@@ -1,7 +0,0 @@
- id: lint
name: ruff lint
description: Run ruff to lint Python files.
entry: ruff
language: python
types_or: [python]
pass_filenames: true

1654
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +1,32 @@
[package]
name = "ruff"
version = "0.0.40"
version = "0.0.44"
edition = "2021"
[lib]
name = "ruff"
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.83"
anyhow = { version = "1.0.60" }
bincode = { version = "1.3.3" }
cacache = { version = "10.0.1" }
chrono = { version = "0.4.21" }
clap = { version = "3.2.16", features = ["derive"] }
clearscreen = { version = "1.0.10" }
colored = { version = "2.0.0" }
common-path = { version = "1.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.17" }
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 = "3.0.13"
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "7d21c6923a506e79cc041708d83cef925efd33f4" }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" }
toml = { version = "0.5.9" }
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true }
walkdir = { version = "2.3.2" }
[dev-dependencies]
insta = { version = "1.19.1", features = ["yaml"] }
[features]
default = ["update-informer"]
update-informer = ["dep:update-informer"]
[profile.release]
panic = "abort"
lto = "thin"

198
README.md
View File

@@ -1,7 +1,9 @@
# ruff
[![image](https://img.shields.io/pypi/v/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/charliermarsh/ruff/workflows/CI/badge.svg)](https://github.com/charliermarsh/ruff/actions)
[![PyPI version](https://badge.fury.io/py/ruff.svg)](https://badge.fury.io/py/ruff)
An extremely fast Python linter, written in Rust.
@@ -20,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).
@@ -52,14 +52,14 @@ You can run ruff in `--watch` mode to automatically re-run on-change:
ruff path/to/code/ --watch
```
ruff also works with [Pre-Commit](https://pre-commit.com) (requires Cargo on system):
ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.40
hooks:
- id: lint
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.40
hooks:
- id: lint
```
## Configuration
@@ -86,7 +86,7 @@ ruff path/to/code/ --select F401 F403
See `ruff --help` for more:
```shell
ruff (v0.0.40)
ruff (v0.0.44) 0.0.44
An extremely fast Python linter.
USAGE:
@@ -117,8 +117,14 @@ OPTIONS:
Disable all logging (but still exit with status code "1" upon detecting errors)
--select <SELECT>...
List of error codes to enable
--show-files
See the files ruff will be run against with the current configuration options
--show-settings
See ruff's settings
-v, --verbose
Enable verbose logging
-V, --version
Print version information
-w, --watch
Run in watch mode by re-running whenever files change
```
@@ -128,38 +134,31 @@ Exclusions are based on globs, and can be either:
- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the
tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching
`foo_*.py` ).
- Relative patterns, like `./directory/foo.py` (to exclude that specific file) or `./directory/*.py`
(to exclude any Python files in `./directory`). Note that these paths are relative to the
directory from which you execute `ruff`, and _not_ the directory of the `pyproject.toml`.
- Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py`
(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`).
### 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
@@ -178,11 +177,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 |
@@ -243,29 +244,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",
]
```
@@ -274,17 +275,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:
@@ -296,11 +301,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:
@@ -310,66 +312,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

24
demo/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

2
demo/.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
dist
node_modules

7
demo/index.css Normal file
View File

@@ -0,0 +1,7 @@
body,
html,
#root {
margin: 0;
width: 100%;
height: 100%;
}

29
demo/index.html Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="An in-browser playground for Ruff, an extremely fast Python linter written in Rust."
/>
<meta name="keywords" content="ruff, python, rust, webassembly, wasm" />
<title>Ruff Playground</title>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛠️</text></svg>"
/>
</head>
<body>
<div id="root"></div>
<div style="display: flex; position: fixed; right: 16px; top: 16px">
<a href="https://GitHub.com/charliermarsh/ruff"
><img
src="https://img.shields.io/github/stars/charliermarsh/ruff.svg?style=social&label=GitHub&maxAge=2592000&?logoWidth=100"
alt="GitHub stars"
style="width: 120px"
/></a>
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2542
demo/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
demo/package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"push": "wrangler pages publish dist --project-name=ruff"
},
"dependencies": {
"@monaco-editor/react": "^4.4.5",
"monaco-editor": "^0.34.0",
"prettier": "^2.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.1.0",
"typescript": "^4.6.4",
"vite": "^3.1.0"
}
}

1
demo/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

69
demo/src/App.tsx Normal file
View File

@@ -0,0 +1,69 @@
import Editor, { Monaco } from "@monaco-editor/react";
import { editor, MarkerSeverity } from "monaco-editor/esm/vs/editor/editor.api";
import React, { useCallback, useEffect, useState } from "react";
import { DEFAULT_SOURCE } from "./constants";
import init, { check } from "./pkg/ruff.js";
import { Check } from "./types";
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
function App() {
const [initialized, setInitialized] = useState<boolean>(false);
const [_, setCounter] = useState(0);
const [monaco, setMonaco] = useState<Monaco | null>(null);
// Load the WASM module.
useEffect(() => {
init().then(() => setInitialized(true));
}, []);
const handleEditorChange = useCallback(
() => setCounter((counter) => counter + 1),
[]
);
const handleEditorDidMount = useCallback(
(editor: IStandaloneCodeEditor, monaco: Monaco) => {
setMonaco(monaco);
},
[]
);
if (initialized) {
if (monaco) {
const editor = monaco.editor;
if (editor) {
const model = editor.getModels()[0];
if (model) {
const checks: Check[] = JSON.parse(check(model.getValue()));
editor.setModelMarkers(
model,
"owner",
checks.map((check) => ({
startLineNumber: check.location.row,
startColumn: check.location.column,
endLineNumber: check.location.row,
endColumn: check.location.column,
message: `${check.code}: ${check.message}`,
severity: MarkerSeverity.Error,
}))
);
}
}
}
}
return (
<Editor
height={"100%"}
width={"100%"}
path={"ruff"}
options={{ readOnly: false, minimap: { enabled: false } }}
defaultLanguage="python"
defaultValue={DEFAULT_SOURCE}
theme={"light"}
onMount={handleEditorDidMount}
onChange={handleEditorChange}
/>
);
}
export default App;

View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" class="iconify iconify--logos"
width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

55
demo/src/constants.ts Normal file
View File

@@ -0,0 +1,55 @@
export const DEFAULT_SOURCE = `#: E721
if type(res) == type(42):
pass
#: E721
if type(res) != type(""):
pass
#: E721
import types
if res == types.IntType:
pass
#: E721
import types
if type(res) is not types.ListType:
pass
#: E721
assert type(res) == type(False) or type(res) == type(None)
#: E721
assert type(res) == type([])
#: E721
assert type(res) == type(())
#: E721
assert type(res) == type((0,))
#: E721
assert type(res) == type((0))
#: E721
assert type(res) != type((1,))
#: E721
assert type(res) is type((1,))
#: E721
assert type(res) is not type((1,))
#: E721
assert type(res) == type(
[
2,
]
)
#: E721
assert type(res) == type(())
#: E721
assert type(res) == type((0,))
#: Okay
import types
if isinstance(res, int):
pass
if isinstance(res, str):
pass
if isinstance(res, types.MethodType):
pass
if type(a) != type(b) or type(a) == type(ccc):
pass
`;

10
demo/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "../index.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

5
demo/src/types.ts Normal file
View File

@@ -0,0 +1,5 @@
export type Check = {
code: string;
message: string;
location: { row: number; column: number };
};

1
demo/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

21
demo/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

9
demo/tsconfig.node.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

7
demo/vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});

View File

@@ -1,56 +1,14 @@
/// Generate a Markdown-compatible table of supported lint rules.
use ruff::checks::{CheckKind, RejectedCmpop};
use ruff::checks::{CheckCode, ALL_CHECK_CODES};
fn main() {
let mut check_kinds: Vec<CheckKind> = vec![
CheckKind::AmbiguousClassName("...".to_string()),
CheckKind::AmbiguousFunctionName("...".to_string()),
CheckKind::AmbiguousVariableName("...".to_string()),
CheckKind::AssertTuple,
CheckKind::BreakOutsideLoop,
CheckKind::ContinueOutsideLoop,
CheckKind::DefaultExceptNotLast,
CheckKind::DoNotAssignLambda,
CheckKind::DoNotUseBareExcept,
CheckKind::DuplicateArgumentName,
CheckKind::FStringMissingPlaceholders,
CheckKind::ForwardAnnotationSyntaxError("...".to_string()),
CheckKind::FutureFeatureNotDefined("...".to_string()),
CheckKind::IOError("...".to_string()),
CheckKind::IfTuple,
CheckKind::ImportStarNotPermitted("...".to_string()),
CheckKind::ImportStarUsage("...".to_string()),
CheckKind::InvalidPrintSyntax,
CheckKind::IsLiteral,
CheckKind::LateFutureImport,
CheckKind::LineTooLong(89, 88),
CheckKind::ModuleImportNotAtTopOfFile,
CheckKind::MultiValueRepeatedKeyLiteral,
CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
CheckKind::NoAssertEquals,
CheckKind::NoneComparison(RejectedCmpop::Eq),
CheckKind::NotInTest,
CheckKind::NotIsTest,
CheckKind::RaiseNotImplemented,
CheckKind::ReturnOutsideFunction,
CheckKind::SyntaxError("...".to_string()),
CheckKind::TooManyExpressionsInStarredAssignment,
CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
CheckKind::TwoStarredExpressions,
CheckKind::TypeComparison,
CheckKind::UndefinedExport("...".to_string()),
CheckKind::UndefinedLocal("...".to_string()),
CheckKind::UndefinedName("...".to_string()),
CheckKind::UnusedImport("...".to_string()),
CheckKind::UnusedVariable("...".to_string()),
CheckKind::UselessObjectInheritance("...".to_string()),
CheckKind::YieldOutsideFunction,
];
check_kinds.sort_by_key(|check_kind| check_kind.code());
let mut check_codes: Vec<CheckCode> = ALL_CHECK_CODES.to_vec();
check_codes.sort();
println!("| Code | Name | Message |");
println!("| ---- | ----- | ------- |");
for check_kind in check_kinds {
for check_code in check_codes {
let check_kind = check_code.kind();
println!(
"| {} | {} | {} |",
check_kind.code().as_str(),

16
index.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8"/>
<title>hello-wasm example</title>
</head>
<body>
<script type="module">
import init, {check} from "./pkg/ruff.js";
init().then(() => {
console.log(check("if (1, 2): pass"));
});
</script>
</body>
</html>

View File

@@ -4,3 +4,6 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
"""
_ = "---------------------------------------------------------------------------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}'
)

View File

@@ -22,3 +22,10 @@ def g():
bar = (1, 2)
(c, d) = bar
(x, y) = baz = bar
def h():
locals()
x = 1

View File

@@ -3,49 +3,5 @@ line-length = 88
extend-exclude = [
"excluded.py",
"migrations",
"./resources/test/fixtures/directory/also_excluded.py",
]
select = [
"E402",
"E501",
"E711",
"E712",
"E713",
"E714",
"E721",
"E722",
"E731",
"E741",
"E742",
"E743",
"E902",
"E999",
"F401",
"F403",
"F404",
"F406",
"F407",
"F541",
"F601",
"F602",
"F621",
"F622",
"F631",
"F632",
"F633",
"F634",
"F701",
"F702",
"F704",
"F706",
"F707",
"F722",
"F821",
"F822",
"F823",
"F831",
"F841",
"F901",
"R001",
"R002",
"directory/also_excluded.py",
]

View File

@@ -7,7 +7,7 @@ use rustpython_parser::ast::{
};
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::{Binding, BindingKind, Scope};
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,9 +71,16 @@ 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!(
scope.kind,
ScopeKind::Function(FunctionScope { uses_locals: true })
) {
return checks;
}
for (name, binding) in scope.values.iter() {
// TODO(charlie): Ignore if using `locals`.
if binding.used.is_none()
@@ -78,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),
));
}
}
@@ -292,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![];
@@ -306,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),
))
}
}
@@ -314,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),
))
}
}
@@ -333,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![];
@@ -352,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),
));
}
}
@@ -372,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),
));
}
}
@@ -398,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),
));
}
}
@@ -418,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),
));
}
}
@@ -521,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;
@@ -556,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;
@@ -582,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
}
@@ -593,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;
@@ -619,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

@@ -101,11 +101,13 @@ pub fn in_nested_block(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
/// Check if a node represents an unpacking assignment.
pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
if let StmtKind::Assign { targets, value, .. } = &stmt.node {
for child in targets {
match &child.node {
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. } => {}
_ => return false,
}
if !targets.iter().any(|child| {
matches!(
child.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
)
}) {
return false;
}
match &value.node {
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. } => return false,

View File

@@ -8,10 +8,15 @@ fn id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed)
}
#[derive(Clone, Debug, Default)]
pub struct FunctionScope {
pub uses_locals: bool,
}
#[derive(Clone, Debug)]
pub enum ScopeKind {
Class,
Function,
Function(FunctionScope),
Generator,
Module,
}
@@ -20,6 +25,7 @@ pub enum ScopeKind {
pub struct Scope {
pub id: usize,
pub kind: ScopeKind,
pub import_starred: bool,
pub values: BTreeMap<String, Binding>,
}
@@ -28,6 +34,7 @@ impl Scope {
Scope {
id: id(),
kind,
import_starred: false,
values: BTreeMap::new(),
}
}
@@ -39,6 +46,7 @@ pub enum BindingKind {
Argument,
Assignment,
Binding,
LoopVar,
Builtin,
ClassDefinition,
Definition,
@@ -57,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

@@ -1,11 +1,3 @@
use std::fs;
use std::path::Path;
use anyhow::Result;
use rustpython_parser::ast::Location;
use crate::checks::{Check, Fix};
#[derive(Hash)]
pub enum Mode {
Generate,
@@ -21,196 +13,3 @@ impl From<bool> for Mode {
}
}
}
/// Auto-fix errors in a file, and write the fixed source code to disk.
pub fn fix_file(checks: &mut [Check], contents: &str, path: &Path) -> Result<()> {
if checks.iter().all(|check| check.fix.is_none()) {
return Ok(());
}
let output = apply_fixes(
checks.iter_mut().filter_map(|check| check.fix.as_mut()),
contents,
);
fs::write(path, output).map_err(|e| e.into())
}
/// Apply a series of fixes.
fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) -> String {
let lines: Vec<&str> = contents.lines().collect();
let mut output = "".to_string();
let mut last_pos: Location = Location::new(0, 0);
for fix in fixes {
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
if last_pos > fix.start {
continue;
}
if fix.start.row() > last_pos.row() {
if last_pos.row() > 0 || last_pos.column() > 0 {
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
output.push('\n');
}
for line in &lines[last_pos.row()..fix.start.row() - 1] {
output.push_str(line);
output.push('\n');
}
output.push_str(&lines[fix.start.row() - 1][..fix.start.column() - 1]);
output.push_str(&fix.content);
} else {
output.push_str(
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.start.column() - 1],
);
output.push_str(&fix.content);
}
last_pos = fix.end;
fix.applied = true;
}
if last_pos.row() > 0 || last_pos.column() > 0 {
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
output.push('\n');
}
for line in &lines[last_pos.row()..] {
output.push_str(line);
output.push('\n');
}
output
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use rustpython_parser::ast::Location;
use crate::autofix::fixer::apply_fixes;
use crate::checks::Fix;
#[test]
fn empty_file() -> Result<()> {
let mut fixes = vec![];
let actual = apply_fixes(fixes.iter_mut(), "");
let expected = "";
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn apply_single_replacement() -> Result<()> {
let mut fixes = vec![Fix {
content: "Bar".to_string(),
start: Location::new(1, 9),
end: Location::new(1, 15),
applied: false,
}];
let actual = apply_fixes(
fixes.iter_mut(),
"class A(object):
...
",
);
let expected = "class A(Bar):
...
";
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn apply_single_removal() -> Result<()> {
let mut fixes = vec![Fix {
content: "".to_string(),
start: Location::new(1, 8),
end: Location::new(1, 16),
applied: false,
}];
let actual = apply_fixes(
fixes.iter_mut(),
"class A(object):
...
",
);
let expected = "class A:
...
";
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn apply_double_removal() -> Result<()> {
let mut fixes = vec![
Fix {
content: "".to_string(),
start: Location::new(1, 8),
end: Location::new(1, 17),
applied: false,
},
Fix {
content: "".to_string(),
start: Location::new(1, 17),
end: Location::new(1, 24),
applied: false,
},
];
let actual = apply_fixes(
fixes.iter_mut(),
"class A(object, object):
...
",
);
let expected = "class A:
...
";
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn ignore_overlapping_fixes() -> Result<()> {
let mut fixes = vec![
Fix {
content: "".to_string(),
start: Location::new(1, 8),
end: Location::new(1, 16),
applied: false,
},
Fix {
content: "ignored".to_string(),
start: Location::new(1, 10),
end: Location::new(1, 12),
applied: false,
},
];
let actual = apply_fixes(
fixes.iter_mut(),
"class A(object):
...
",
);
let expected = "class A:
...
";
assert_eq!(actual, expected);
Ok(())
}
}

View File

@@ -1,152 +0,0 @@
use std::collections::hash_map::DefaultHasher;
use std::fs::{create_dir_all, File, Metadata};
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::path::Path;
use anyhow::Result;
use cacache::Error::EntryNotFound;
use filetime::FileTime;
use log::error;
use serde::{Deserialize, Serialize};
use crate::autofix::fixer;
use crate::message::Message;
use crate::settings::Settings;
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Serialize, Deserialize)]
struct CacheMetadata {
mtime: i64,
}
#[derive(Serialize)]
struct CheckResultRef<'a> {
metadata: &'a CacheMetadata,
messages: &'a [Message],
}
#[derive(Deserialize)]
struct CheckResult {
metadata: CacheMetadata,
messages: Vec<Message>,
}
pub enum Mode {
ReadWrite,
ReadOnly,
WriteOnly,
None,
}
impl Mode {
fn allow_read(&self) -> bool {
match self {
Mode::ReadWrite => true,
Mode::ReadOnly => true,
Mode::WriteOnly => false,
Mode::None => false,
}
}
fn allow_write(&self) -> bool {
match self {
Mode::ReadWrite => true,
Mode::ReadOnly => false,
Mode::WriteOnly => true,
Mode::None => false,
}
}
}
impl From<bool> for Mode {
fn from(value: bool) -> Self {
match value {
true => Mode::ReadWrite,
false => Mode::None,
}
}
}
fn cache_dir() -> &'static str {
"./.ruff_cache"
}
fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String {
let mut hasher = DefaultHasher::new();
settings.hash(&mut hasher);
autofix.hash(&mut hasher);
format!(
"{}@{}@{}",
path.canonicalize().unwrap().to_string_lossy(),
VERSION,
hasher.finish()
)
}
pub fn init() -> Result<()> {
let gitignore_path = Path::new(cache_dir()).join(".gitignore");
if gitignore_path.exists() {
return Ok(());
}
create_dir_all(cache_dir())?;
let mut file = File::create(gitignore_path)?;
file.write_all(b"*").map_err(|e| e.into())
}
pub fn get(
path: &Path,
metadata: &Metadata,
settings: &Settings,
autofix: &fixer::Mode,
mode: &Mode,
) -> Option<Vec<Message>> {
if !mode.allow_read() {
return None;
};
match cacache::read_sync(cache_dir(), cache_key(path, settings, autofix)) {
Ok(encoded) => match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
metadata: CacheMetadata { mtime },
messages,
}) => {
if FileTime::from_last_modification_time(metadata).unix_seconds() == mtime {
return Some(messages);
}
}
Err(e) => error!("Failed to deserialize encoded cache entry: {e:?}"),
},
Err(EntryNotFound(_, _)) => {}
Err(e) => error!("Failed to read from cache: {e:?}"),
}
None
}
pub fn set(
path: &Path,
metadata: &Metadata,
settings: &Settings,
autofix: &fixer::Mode,
messages: &[Message],
mode: &Mode,
) {
if !mode.allow_write() {
return;
};
let check_result = CheckResultRef {
metadata: &CacheMetadata {
mtime: FileTime::from_last_modification_time(metadata).unix_seconds(),
},
messages,
};
if let Err(e) = cacache::write_sync(
cache_dir(),
cache_key(path, settings, autofix),
bincode::serialize(&check_result).unwrap(),
) {
error!("Failed to write to cache: {e:?}")
}
}

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, 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);
}
}
@@ -646,6 +679,21 @@ where
self.checks.push(check)
}
}
if let ExprKind::Name { id, ctx } = &func.node {
if id == "locals" && matches!(ctx, ExprContext::Load) {
let scope = &mut self.scopes[*(self
.scope_stack
.last_mut()
.expect("No current scope found."))];
if matches!(
scope.kind,
ScopeKind::Function(FunctionScope { uses_locals: false })
) {
scope.kind = ScopeKind::Function(FunctionScope { uses_locals: true });
}
}
}
}
ExprKind::Dict { keys, .. } => {
let check_repeated_literals = self.settings.select.contains(&CheckCode::F601);
@@ -655,6 +703,7 @@ where
keys,
check_repeated_literals,
check_repeated_variables,
self,
));
}
}
@@ -667,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
@@ -683,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,
@@ -719,6 +770,7 @@ where
operand,
check_not_in,
check_not_is,
self,
));
}
}
@@ -736,6 +788,7 @@ where
comparators,
check_none_comparisons,
check_true_false_comparisons,
self,
));
}
@@ -744,7 +797,7 @@ where
left,
ops,
comparators,
expr.location,
self.locate_check(expr.location),
));
}
@@ -752,7 +805,7 @@ where
self.checks.extend(checks::check_type_comparison(
ops,
comparators,
expr.location,
self.locate_check(expr.location),
));
}
}
@@ -917,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);
}
}
@@ -1019,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());
@@ -1083,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);
}
@@ -1099,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) {
@@ -1115,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) {
@@ -1124,7 +1221,7 @@ impl<'a> Checker<'a> {
}
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
expr.location,
self.locate_check(expr.location),
))
}
}
@@ -1136,17 +1233,17 @@ impl<'a> Checker<'a> {
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if self.settings.select.contains(&CheckCode::F823)
&& matches!(current.kind, ScopeKind::Function)
&& matches!(current.kind, ScopeKind::Function(_))
&& !current.values.contains_key(id)
{
for scope in self.scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function | ScopeKind::Module) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if let Some(binding) = scope.values.get(id) {
if let Some((scope_id, location)) = binding.used {
if scope_id == current.id {
self.checks.push(Check::new(
CheckKind::UndefinedLocal(id.clone()),
location,
self.locate_check(location),
));
}
}
@@ -1171,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 {
@@ -1228,7 +1336,7 @@ impl<'a> Checker<'a> {
{
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
expr.location,
self.locate_check(expr.location),
))
}
}
@@ -1253,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),
));
}
}
@@ -1266,7 +1374,7 @@ impl<'a> Checker<'a> {
while let Some((stmt, scopes, parents)) = self.deferred_functions.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.push_scope(Scope::new(ScopeKind::Function));
self.push_scope(Scope::new(ScopeKind::Function(Default::default())));
match &stmt.node {
StmtKind::FunctionDef { body, args, .. }
@@ -1290,7 +1398,7 @@ impl<'a> Checker<'a> {
while let Some((expr, scopes, parents)) = self.deferred_lambdas.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.push_scope(Scope::new(ScopeKind::Function));
self.push_scope(Scope::new(ScopeKind::Function(Default::default())));
if let ExprKind::Lambda { args, body } = &expr.node {
self.visit_arguments(args);
@@ -1305,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;
}
@@ -1330,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),
));
}
}
@@ -1359,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

@@ -35,7 +35,7 @@ pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings)
// Enforce line length.
if enforce_line_too_long {
let line_length = line.len();
let line_length = line.chars().count();
if should_enforce_line_length(line, line_length, settings.line_length) {
let check = Check::new(
CheckKind::LineTooLong(line_length, settings.line_length),
@@ -53,3 +53,31 @@ pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings)
}
checks.extend(line_checks);
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::check_lines;
use super::*;
#[test]
fn e501_non_ascii_char() {
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
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);
return checks;
};
assert!(!check_with_max_line_length(6).is_empty());
assert!(check_with_max_line_length(7).is_empty());
}
}

View File

@@ -6,7 +6,54 @@ use regex::Regex;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord)]
pub const ALL_CHECK_CODES: [CheckCode; 44] = [
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::R001,
CheckCode::R002,
];
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord)]
pub enum CheckCode {
E402,
E501,
@@ -23,8 +70,10 @@ pub enum CheckCode {
E902,
E999,
F401,
F402,
F403,
F404,
F405,
F406,
F407,
F541,
@@ -72,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),
@@ -122,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",
@@ -160,6 +213,56 @@ impl CheckCode {
_ => &LintSource::AST,
}
}
/// A placeholder representation of the CheckKind for the check.
pub fn kind(&self) -> CheckKind {
match self {
CheckCode::E402 => CheckKind::ModuleImportNotAtTopOfFile,
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::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::F831 => CheckKind::DuplicateArgumentName,
CheckCode::F841 => CheckKind::UnusedVariable("...".to_string()),
CheckCode::F901 => CheckKind::RaiseNotImplemented,
CheckCode::R001 => CheckKind::UselessObjectInheritance("...".to_string()),
CheckCode::R002 => CheckKind::NoAssertEquals,
}
}
}
#[allow(clippy::upper_case_acronyms)]
@@ -192,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,
@@ -240,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",
@@ -290,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,
@@ -356,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()

147
src/fs.rs
View File

@@ -1,147 +0,0 @@
use std::env;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use anyhow::Result;
use glob::Pattern;
use log::debug;
use path_absolutize::Absolutize;
use walkdir::{DirEntry, WalkDir};
fn is_excluded(path: &Path, exclude: &[Pattern]) -> bool {
// Check the basename.
if let Some(file_name) = path.file_name() {
if let Some(file_name) = file_name.to_str() {
for pattern in exclude {
if pattern.matches(file_name) {
return true;
}
}
}
}
// Check the complete path.
if let Some(file_name) = path.to_str() {
for pattern in exclude {
if pattern.matches(file_name) {
return true;
}
}
}
false
}
fn is_included(path: &Path) -> bool {
let file_name = path.to_string_lossy();
file_name.ends_with(".py") || file_name.ends_with(".pyi")
}
pub fn iter_python_files<'a>(
path: &'a PathBuf,
exclude: &'a [Pattern],
extend_exclude: &'a [Pattern],
) -> impl Iterator<Item = DirEntry> + 'a {
WalkDir::new(normalize_path(path))
.follow_links(true)
.into_iter()
.filter_entry(|entry| {
if exclude.is_empty() && extend_exclude.is_empty() {
return true;
}
let path = entry.path();
if is_excluded(path, exclude) {
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if is_excluded(path, extend_exclude) {
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
} else {
true
}
})
.filter_map(|entry| entry.ok())
.filter(|entry| {
let path = entry.path();
is_included(path)
})
}
pub fn normalize_path(path: &PathBuf) -> PathBuf {
if path == Path::new(".") || path == Path::new("..") {
return path.clone();
}
if let Ok(path) = path.absolutize() {
if let Ok(root) = env::current_dir() {
if let Ok(path) = path.strip_prefix(root) {
return Path::new(".").join(path);
}
}
}
path.clone()
}
pub fn read_file(path: &Path) -> Result<String> {
let file = File::open(path)?;
let mut buf_reader = BufReader::new(file);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
Ok(contents)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use glob::Pattern;
use crate::fs::{is_excluded, is_included};
#[test]
fn inclusions() {
let path = Path::new("foo/bar/baz.py");
assert!(is_included(path));
let path = Path::new("foo/bar/baz.pyi");
assert!(is_included(path));
let path = Path::new("foo/bar/baz.js");
assert!(!is_included(path));
let path = Path::new("foo/bar/baz");
assert!(!is_included(path));
}
#[test]
fn exclusions() {
let path = Path::new("foo");
let exclude = vec![Pattern::new("foo").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar");
let exclude = vec![Pattern::new("bar").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("baz.py").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar");
let exclude = vec![Pattern::new("foo/bar").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("foo/bar/baz.py").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("foo/bar/*.py").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("baz").unwrap()];
assert!(!is_excluded(path, &exclude));
}
}

View File

@@ -1,16 +1,73 @@
extern crate core;
use rustpython_parser::ast::Location;
use std::path::Path;
use rustpython_parser::parser;
use wasm_bindgen::prelude::*;
use crate::check_ast::check_ast;
use crate::check_lines::check_lines;
use crate::checks::{Check, CheckCode, CheckKind, ALL_CHECK_CODES};
use crate::settings::Settings;
mod ast;
mod autofix;
pub mod cache;
pub mod check_ast;
mod check_lines;
pub mod checks;
pub mod fs;
pub mod linter;
pub mod logging;
pub mod message;
pub mod printer;
mod pyproject;
mod python;
pub mod settings;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Message {
code: CheckCode,
message: String,
location: Location,
}
#[wasm_bindgen]
extern "C" {
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn check(contents: &str) -> String {
let settings = Settings::for_rules(ALL_CHECK_CODES.to_vec());
let autofix = autofix::fixer::Mode::None;
// Aggregate all checks.
let mut checks: Vec<Check> = vec![];
// Run the AST-based checks.
match parser::parse_program(contents, "<filename>") {
Ok(python_ast) => checks.extend(check_ast(
&python_ast,
contents,
&settings,
&autofix,
Path::new("<filename>"),
)),
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);
let messages: Vec<Message> = checks
.into_iter()
.map(|check| Message {
code: check.kind.code().clone(),
message: check.kind.body(),
location: check.location,
})
.collect();
serde_json::to_string(&messages).unwrap()
}

View File

@@ -1,799 +0,0 @@
use std::path::Path;
use anyhow::Result;
use log::debug;
use rustpython_parser::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::message::Message;
use crate::settings::Settings;
use crate::{cache, fs};
fn check_path(
path: &Path,
contents: &str,
settings: &Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
// Aggregate all checks.
let mut checks: Vec<Check> = vec![];
// 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));
}
// Run the lines-based checks.
check_lines(&mut checks, contents, settings);
Ok(checks)
}
pub fn lint_path(
path: &Path,
settings: &Settings,
mode: &cache::Mode,
autofix: &fixer::Mode,
) -> Result<Vec<Message>> {
let metadata = path.metadata()?;
// Check the cache.
if let Some(messages) = cache::get(path, &metadata, settings, autofix, mode) {
debug!("Cache hit for: {}", path.to_string_lossy());
return Ok(messages);
}
// Read the file from disk.
let contents = fs::read_file(path)?;
// Generate checks.
let mut checks = check_path(path, &contents, settings, autofix)?;
// Apply autofix.
if matches!(autofix, fixer::Mode::Apply) {
fix_file(&mut checks, &contents, path)?;
};
// Convert to messages.
let messages: Vec<Message> = checks
.into_iter()
.map(|check| Message {
kind: check.kind,
fixed: check.fix.map(|fix| fix.applied).unwrap_or_default(),
location: check.location,
filename: path.to_string_lossy().to_string(),
})
.collect();
cache::set(path, &metadata, settings, autofix, &messages, mode);
Ok(messages)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::path::Path;
use anyhow::Result;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::fs;
use crate::linter;
use crate::settings;
fn check_path(
path: &Path,
settings: &settings::Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
linter::check_path(path, &contents, 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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&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(
Path::new("./resources/test/fixtures/F403.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F403]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&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(
Path::new("./resources/test/fixtures/F406.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F406]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
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]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -1,37 +0,0 @@
use anyhow::Result;
use fern;
#[macro_export]
macro_rules! tell_user {
($($arg:tt)*) => {
println!(
"[{}] {}",
chrono::Local::now()
.format("%H:%M:%S %p")
.to_string()
.dimmed(),
format_args!($($arg)*)
)
}
}
pub fn set_up_logging(verbose: bool) -> Result<()> {
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(),
record.level(),
message
))
})
.level(if verbose {
log::LevelFilter::Debug
} else {
log::LevelFilter::Info
})
.chain(std::io::stdout())
.apply()
.map_err(|e| e.into())
}

View File

@@ -1,239 +0,0 @@
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::Instant;
use anyhow::Result;
use clap::{Parser, ValueHint};
use colored::Colorize;
use glob::Pattern;
use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use walkdir::DirEntry;
use ::ruff::cache;
use ::ruff::checks::CheckCode;
use ::ruff::checks::CheckKind;
use ::ruff::fs::iter_python_files;
use ::ruff::linter::lint_path;
use ::ruff::logging::set_up_logging;
use ::ruff::message::Message;
use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::settings::Settings;
use ::ruff::tell_user;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Parser)]
#[clap(name = format!("{CARGO_PKG_NAME} (v{CARGO_PKG_VERSION})"))]
#[clap(about = "An extremely fast Python linter.", long_about = None)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::AnyPath, required = true)]
files: Vec<PathBuf>,
/// Enable verbose logging.
#[clap(short, long, action)]
verbose: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[clap(short, long, action)]
quiet: bool,
/// Exit with status code "0", even upon detecting errors.
#[clap(short, long, action)]
exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[clap(short, long, action)]
watch: bool,
/// Attempt to automatically fix lint errors.
#[clap(short, long, action)]
fix: bool,
/// Disable cache reads.
#[clap(short, long, action)]
no_cache: bool,
/// List of error codes to enable.
#[clap(long, multiple = true)]
select: Vec<CheckCode>,
/// List of error codes to ignore.
#[clap(long, multiple = true)]
ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[clap(long, multiple = true)]
exclude: Vec<Pattern>,
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[clap(long, multiple = true)]
extend_exclude: Vec<Pattern>,
/// Output serialization format for error messages.
#[clap(long, arg_enum, default_value_t=SerializationFormat::Text)]
format: SerializationFormat,
}
#[cfg(feature = "update-informer")]
fn check_for_updates() {
use update_informer::{registry, Check};
let informer = update_informer::new(registry::PyPI, CARGO_PKG_NAME, CARGO_PKG_VERSION);
if let Some(new_version) = informer.check_version().ok().flatten() {
let msg = format!(
"A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",
pkg_name = CARGO_PKG_NAME.italic().cyan(),
pkg_version = CARGO_PKG_VERSION,
new_version = new_version.to_string().green()
);
let cmd = format!(
"Run to update: {cmd} {pkg_name}",
cmd = "pip3 install --upgrade".green(),
pkg_name = CARGO_PKG_NAME.green()
);
println!("\n{msg}\n{cmd}");
}
}
fn run_once(
files: &[PathBuf],
settings: &Settings,
cache: bool,
autofix: bool,
) -> Result<Vec<Message>> {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<DirEntry> = 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 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(),
}]
} else {
error!("Failed to check {}: {e:?}", entry.path().to_string_lossy());
vec![]
}
})
})
.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);
Ok(messages)
}
fn inner_main() -> Result<ExitCode> {
let cli = Cli::parse();
set_up_logging(cli.verbose)?;
let mut settings = Settings::from_paths(&cli.files);
let mut printer = Printer::new(cli.format);
if !cli.select.is_empty() {
settings.select(cli.select);
}
if !cli.ignore.is_empty() {
settings.ignore(&cli.ignore);
}
if !cli.exclude.is_empty() {
settings.exclude = cli.exclude;
}
if !cli.extend_exclude.is_empty() {
settings.extend_exclude = cli.extend_exclude;
}
cache::init()?;
if cli.watch {
if cli.fix {
println!("Warning: --fix is not enabled in watch mode.");
}
if cli.format != SerializationFormat::Text {
println!("Warning: --format 'text' is used in watch mode.");
}
// Perform an initial run instantly.
printer.clear_screen()?;
tell_user!("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
printer.write_continuously(&messages)?;
}
// Configure the file watcher.
let (tx, rx) = channel();
let mut watcher = raw_watcher(tx)?;
for file in &cli.files {
watcher.watch(file, RecursiveMode::Recursive)?;
}
loop {
match rx.recv() {
Ok(e) => {
if let Some(path) = e.path {
if path.to_string_lossy().ends_with(".py") {
printer.clear_screen()?;
tell_user!("File change detected...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
printer.write_continuously(&messages)?;
}
}
}
}
Err(e) => return Err(e.into()),
}
}
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
if !cli.quiet {
printer.write_once(&messages)?;
}
#[cfg(feature = "update-informer")]
check_for_updates();
if !messages.is_empty() && !cli.exit_zero {
return Ok(ExitCode::FAILURE);
}
}
Ok(ExitCode::SUCCESS)
}
fn main() -> ExitCode {
match inner_main() {
Ok(code) => code,
Err(_) => ExitCode::FAILURE,
}
}

View File

@@ -1,49 +0,0 @@
use std::cmp::Ordering;
use std::fmt;
use colored::Colorize;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use crate::checks::CheckKind;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Message {
pub kind: CheckKind,
pub fixed: bool,
pub location: Location,
pub filename: String,
}
impl Ord for Message {
fn cmp(&self, other: &Self) -> Ordering {
(&self.filename, self.location.row(), self.location.column()).cmp(&(
&other.filename,
other.location.row(),
other.location.column(),
))
}
}
impl PartialOrd for Message {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}{}{}{}{}{} {} {}",
self.filename.white().bold(),
":".cyan(),
self.location.row(),
":".cyan(),
self.location.column(),
":".cyan(),
self.kind.code().as_str().red().bold(),
self.kind.body()
)
}
}

View File

@@ -1,80 +0,0 @@
use colored::Colorize;
use anyhow::Result;
use clap::ValueEnum;
use crate::message::Message;
use crate::tell_user;
#[derive(Clone, Copy, ValueEnum, PartialEq, Eq, Debug)]
pub enum SerializationFormat {
Text,
Json,
}
pub struct Printer {
format: SerializationFormat,
}
impl Printer {
pub fn new(format: SerializationFormat) -> Self {
Self { format }
}
pub fn write_once(&mut self, messages: &[Message]) -> Result<()> {
let (fixed, outstanding): (Vec<&Message>, Vec<&Message>) =
messages.iter().partition(|message| message.fixed);
let num_fixable = outstanding
.iter()
.filter(|message| message.kind.fixable())
.count();
match self.format {
SerializationFormat::Json => {
println!("{}", serde_json::to_string_pretty(&messages)?)
}
SerializationFormat::Text => {
if !fixed.is_empty() {
println!(
"Found {} error(s) ({} fixed).",
outstanding.len(),
fixed.len()
)
} else {
println!("Found {} error(s).", outstanding.len())
}
for message in outstanding {
println!("{}", message)
}
if num_fixable > 0 {
println!("{num_fixable} potentially fixable with the --fix option.")
}
}
}
Ok(())
}
pub fn write_continuously(&mut self, messages: &[Message]) -> Result<()> {
tell_user!(
"Found {} error(s). Watching for file changes.",
messages.len(),
);
if !messages.is_empty() {
println!();
for message in messages {
println!("{}", message)
}
}
Ok(())
}
pub fn clear_screen(&mut self) -> Result<()> {
clearscreen::clear()?;
Ok(())
}
}

View File

@@ -1,317 +0,0 @@
use std::path::{Path, PathBuf};
use anyhow::Result;
use common_path::common_path_all;
use log::debug;
use serde::Deserialize;
use crate::checks::CheckCode;
use crate::fs;
pub fn load_config(paths: &[PathBuf]) -> Config {
match find_project_root(paths) {
Some(project_root) => match find_pyproject_toml(&project_root) {
Some(path) => {
debug!("Found pyproject.toml at: {:?}", path);
match parse_pyproject_toml(&path) {
Ok(pyproject) => pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default(),
Err(e) => {
println!("Failed to load pyproject.toml: {:?}", e);
println!("Falling back to default configuration...");
Default::default()
}
}
}
None => Default::default(),
},
None => Default::default(),
}
}
#[derive(Debug, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Config {
pub line_length: Option<usize>,
pub exclude: Option<Vec<PathBuf>>,
pub extend_exclude: Option<Vec<PathBuf>>,
pub select: Option<Vec<CheckCode>>,
pub ignore: Option<Vec<CheckCode>>,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct Tools {
ruff: Option<Config>,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct PyProject {
tool: Option<Tools>,
}
fn parse_pyproject_toml(path: &Path) -> Result<PyProject> {
let contents = fs::read_file(path)?;
toml::from_str(&contents).map_err(|e| e.into())
}
fn find_pyproject_toml(path: &Path) -> Option<PathBuf> {
let path_pyproject_toml = path.join("pyproject.toml");
if path_pyproject_toml.is_file() {
return Some(path_pyproject_toml);
}
find_user_pyproject_toml()
}
fn find_user_pyproject_toml() -> Option<PathBuf> {
dirs::home_dir().map(|path| path.join(".ruff"))
}
fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
if let Some(prefix) = common_path_all(sources.iter().map(PathBuf::as_path)) {
for directory in prefix.ancestors() {
if directory.join(".git").is_dir() {
return Some(directory.to_path_buf());
}
if directory.join(".hg").is_dir() {
return Some(directory.to_path_buf());
}
if directory.join("pyproject.toml").is_file() {
return Some(directory.to_path_buf());
}
}
}
None
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use anyhow::Result;
use crate::checks::CheckCode;
use crate::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Config, PyProject, Tools,
};
#[test]
fn deserialize() -> Result<()> {
let pyproject: PyProject = toml::from_str(r#""#)?;
assert_eq!(pyproject.tool, None);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
"#,
)?;
assert_eq!(pyproject.tool, Some(Tools { ruff: None }));
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: None,
extend_exclude: None,
select: None,
ignore: None,
})
})
);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
line-length = 79
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: Some(79),
exclude: None,
extend_exclude: None,
select: None,
ignore: None,
})
})
);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
exclude = ["foo.py"]
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: Some(vec![Path::new("foo.py").to_path_buf()]),
extend_exclude: None,
select: None,
ignore: None,
})
})
);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
select = ["E501"]
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: None,
extend_exclude: None,
select: Some(vec![CheckCode::E501]),
ignore: None,
})
})
);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
ignore = ["E501"]
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: None,
extend_exclude: None,
select: None,
ignore: Some(vec![CheckCode::E501]),
})
})
);
assert!(toml::from_str::<PyProject>(
r#"
[tool.black]
[tool.ruff]
line_length = 79
"#,
)
.is_err());
assert!(toml::from_str::<PyProject>(
r#"
[tool.black]
[tool.ruff]
select = ["E123"]
"#,
)
.is_err());
assert!(toml::from_str::<PyProject>(
r#"
[tool.black]
[tool.ruff]
line-length = 79
other-attribute = 1
"#,
)
.is_err());
Ok(())
}
#[test]
fn find_and_parse_pyproject_toml() -> Result<()> {
let project_root =
find_project_root(&[PathBuf::from("resources/test/fixtures/__init__.py")])
.expect("Unable to find project root.");
assert_eq!(project_root, Path::new("resources/test/fixtures"));
let path = find_pyproject_toml(&project_root).expect("Unable to find pyproject.toml.");
assert_eq!(path, Path::new("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject
.tool
.and_then(|tool| tool.ruff)
.expect("Unable to find tool.ruff.");
assert_eq!(
config,
Config {
line_length: Some(88),
exclude: None,
extend_exclude: Some(vec![
Path::new("excluded.py").to_path_buf(),
Path::new("migrations").to_path_buf(),
Path::new("./resources/test/fixtures/directory/also_excluded.py").to_path_buf()
]),
select: Some(vec![
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::F403,
CheckCode::F404,
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::R001,
CheckCode::R002,
]),
ignore: None,
}
);
Ok(())
}
}

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

@@ -1,21 +1,30 @@
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use glob::Pattern;
use once_cell::sync::Lazy;
use crate::checks::CheckCode;
use crate::pyproject::load_config;
#[derive(Debug)]
pub struct Settings {
pub line_length: usize,
pub exclude: Vec<Pattern>,
pub extend_exclude: Vec<Pattern>,
pub select: BTreeSet<CheckCode>,
}
impl Settings {
pub fn for_rule(check_code: CheckCode) -> Self {
Self {
line_length: 88,
select: BTreeSet::from([check_code]),
}
}
pub fn for_rules(check_codes: Vec<CheckCode>) -> Self {
Self {
line_length: 88,
select: BTreeSet::from_iter(check_codes),
}
}
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
self.line_length.hash(state);
@@ -24,121 +33,3 @@ impl Hash for Settings {
}
}
}
static DEFAULT_EXCLUDE: Lazy<Vec<Pattern>> = Lazy::new(|| {
vec![
Pattern::new(".bzr").unwrap(),
Pattern::new(".direnv").unwrap(),
Pattern::new(".eggs").unwrap(),
Pattern::new(".git").unwrap(),
Pattern::new(".hg").unwrap(),
Pattern::new(".mypy_cache").unwrap(),
Pattern::new(".nox").unwrap(),
Pattern::new(".pants.d").unwrap(),
Pattern::new(".ruff_cache").unwrap(),
Pattern::new(".svn").unwrap(),
Pattern::new(".tox").unwrap(),
Pattern::new(".venv").unwrap(),
Pattern::new("__pypackages__").unwrap(),
Pattern::new("_build").unwrap(),
Pattern::new("buck-out").unwrap(),
Pattern::new("build").unwrap(),
Pattern::new("dist").unwrap(),
Pattern::new("node_modules").unwrap(),
Pattern::new("venv").unwrap(),
]
});
impl Settings {
pub fn from_paths(paths: &[PathBuf]) -> Self {
let config = load_config(paths);
let mut settings = Settings {
line_length: config.line_length.unwrap_or(88),
exclude: config
.exclude
.map(|paths| {
paths
.iter()
.map(|path| {
Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.")
})
.collect()
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
extend_exclude: config
.extend_exclude
.map(|paths| {
paths
.iter()
.map(|path| {
Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.")
})
.collect()
})
.unwrap_or_default(),
select: BTreeSet::from_iter(config.select.unwrap_or_else(|| {
vec![
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::F403,
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,
// Disable refactoring codes by default.
// CheckCode::R001,
// CheckCode::R002,
]
})),
};
if let Some(ignore) = &config.ignore {
settings.ignore(ignore);
}
settings
}
pub fn select(&mut self, codes: Vec<CheckCode>) {
self.select.clear();
for code in codes {
self.select.insert(code);
}
}
pub fn ignore(&mut self, codes: &[CheckCode]) {
for code in codes {
self.select.remove(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: ~