Compare commits

..

135 Commits

Author SHA1 Message Date
Charlie Marsh
b8f517c70e Bump version to 0.0.45 2022-09-22 14:11:09 -04:00
Charlie Marsh
9f601c2abd Document noqa workflows 2022-09-22 14:10:02 -04:00
Charlie Marsh
c0ce0b0c48 Enable automatic noqa insertion (#256) 2022-09-22 13:59:06 -04:00
Charlie Marsh
e5b16973a9 Enable autofix for M001 (#255) 2022-09-22 13:21:03 -04:00
Charlie Marsh
de9ceb2fe1 Only enforce multi-line noqa directives for strings (#258) 2022-09-22 13:09:02 -04:00
Charlie Marsh
38b19b78b7 Enable noqa directives on logical lines (#257) 2022-09-22 12:56:15 -04:00
Charlie Marsh
7043e15b57 Move noqa to a separate module 2022-09-22 09:04:54 -04:00
Charlie Marsh
9594079235 Add --extend-select and --extend-ignore (#254) 2022-09-21 19:56:43 -04:00
Charlie Marsh
732f208e47 Add a lint rule to enforce noqa validity (#253) 2022-09-21 19:56:38 -04:00
Charlie Marsh
32e62d9209 Use specific version tags 2022-09-21 15:11:53 -04:00
Charlie Marsh
d9e4b0cdc1 Implement --show-settings and --show-files (#246) 2022-09-21 15:08:50 -04:00
Charlie Marsh
36fcfad56a Remove empty comment 2022-09-21 13:44:49 -04:00
Charlie Marsh
65d29d9734 Adjust line numbers when reporting rules in f-strings (#244) 2022-09-21 13:42:58 -04:00
Charlie Marsh
1e171ce0e8 Bump version to 0.0.44 2022-09-21 12:25:14 -04:00
Charlie Marsh
2bdc500c61 Re-run cargo insta 2022-09-21 12:24:46 -04:00
Charlie Marsh
f453e429b6 Add a note on parity 2022-09-21 12:24:04 -04:00
Charlie Marsh
73874f4788 Remove proof-of-concept caveat 2022-09-21 12:18:01 -04:00
Charlie Marsh
8846dcdf6a Update README 2022-09-21 12:17:41 -04:00
Charlie Marsh
d827e6e36a Implement F405 (#243) 2022-09-21 12:13:40 -04:00
Harutaka Kawamura
71d9b2ac5f Implement F402 (#221) 2022-09-21 11:12:55 -04:00
Anders Kaseorg
401b53cc45 Handle filesystem errors more consistently (#240) 2022-09-20 23:22:01 -04:00
Anders Kaseorg
aa9c1e255c Simplify check_path type (#239) 2022-09-20 21:11:42 -04:00
Anders Kaseorg
f7fc702b2c Include specified files, even if they lack a .py[i] extension (#238) 2022-09-20 20:53:52 -04:00
Anders Kaseorg
50ca0d7d0a Correctly display the location of parse errors (#237) 2022-09-20 20:53:22 -04:00
Anders Kaseorg
65e0284698 Suppress “Found 0 error(s)” message (#236) 2022-09-20 19:32:39 -04:00
Charlie Marsh
e4f571ea61 Bump version to 0.0.43 2022-09-20 12:26:49 -04:00
Charlie Marsh
4ed88dd245 Follow-up fixes to path absolution (#235) 2022-09-20 12:26:32 -04:00
Charlie Marsh
09b926fd59 Optimize imports 2022-09-20 09:10:39 -04:00
Charlie Marsh
a4869e4974 Update benchmark in README 2022-09-20 07:06:12 -06:00
Charlie Marsh
f53c4fc221 Bump version to 0.0.42 2022-09-19 21:14:17 -06:00
Charlie Marsh
3892a49a97 Bump version to 0.0.41 2022-09-19 21:09:33 -06:00
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
Charlie Marsh
3937885f37 Bump version to 0.0.40 2022-09-16 04:57:21 -04:00
Charlie Marsh
24de97d951 Create cache directory prior to writing .gitignore 2022-09-16 04:56:58 -04:00
Charlie Marsh
06e5b3e457 Bump version to 0.0.39 2022-09-15 21:41:14 -04:00
Charlie Marsh
68a0e6dc19 Remove erroneous test dir 2022-09-15 21:41:00 -04:00
Charlie Marsh
9d4a4478f7 Improve exclusion syntax to match exact files (#209) 2022-09-15 21:40:49 -04:00
Charlie Marsh
6bbf3f46c4 Add .gitignore to .ruff_cache (#208) 2022-09-15 20:40:06 -04:00
Charlie Marsh
4ac4e8c991 Exclude .ruff_cache by default (#207) 2022-09-15 20:39:39 -04:00
Dmitry Dygalo
0091a3ae5f chore: Do not read the same file twice (#206) 2022-09-15 16:05:29 -04:00
Patrick Haller
17b3109a8b Update docs with --format flag (#205) 2022-09-15 16:04:07 -04:00
Charlie Marsh
71520213c1 Allow __path__ in __init__.py (#201) 2022-09-15 09:44:03 -04:00
Charlie Marsh
f24e7a0052 Add trailing period to help message 2022-09-15 09:43:51 -04:00
Patrick Haller
507e9f7ec3 Fix: Structured output Issue Fix (#186) 2022-09-15 09:43:10 -04:00
Charlie Marsh
592c53c8bf Use binding location when reporting F821 errors (#200) 2022-09-14 22:51:07 -04:00
Charlie Marsh
a2df89dedd Bump version to 0.0.38 2022-09-14 22:38:42 -04:00
Charlie Marsh
b8f12d2e79 Raise error when failing to parse (#199) 2022-09-14 22:37:55 -04:00
Charlie Marsh
67b1d0463a Pull in pycodestyle tests for E checks (#195) 2022-09-14 22:22:53 -04:00
Charlie Marsh
d008a181ec Improve default exclusions and support extend-exclude (#188) 2022-09-14 22:21:17 -04:00
Charlie Marsh
6d612a428a Migrate linter tests to insta (#194) 2022-09-14 21:52:44 -04:00
Charlie Marsh
c0cb73ab16 Implement E721 (#193) 2022-09-14 21:10:29 -04:00
Charlie Marsh
2e1eb84cbf Implement F632 (#190) 2022-09-14 18:22:35 -04:00
Charlie Marsh
b03a8728b5 Add support for from __future__ import annotations (#189) 2022-09-14 18:22:19 -04:00
Charlie Marsh
1dd3350a30 Revert "Adding flag and logic for different output format" (#187) 2022-09-14 14:20:02 -04:00
Patrick Haller
bda34945a5 Adding flag and logic for different output format (#185) 2022-09-14 10:43:32 -04:00
Dmitry Dygalo
85dcaa8d3c chore: Avoid collect in inner_main (#184) 2022-09-14 08:16:04 -04:00
Charlie Marsh
4ac74ed0ad Revert erroneous pyproject.toml changes 2022-09-14 08:14:26 -04:00
Dmitry Dygalo
b7e2a4b9a9 feat: Implement InvalidPrintSyntax (F633) (#182) 2022-09-13 21:10:20 -04:00
Dmitry Dygalo
53a7758248 Avoid some allocations (#179) 2022-09-13 10:07:22 -04:00
Dmitry Dygalo
2ba767957d refactor: Use while let Some instead of calling is_empty (#180) 2022-09-13 10:06:49 -04:00
Dmitry Dygalo
08152787e1 chore: Use once_cell instead of lazy_static (#178) 2022-09-13 10:06:21 -04:00
Charlie Marsh
5f77b420cd Bump version to 0.0.37 2022-09-12 21:35:08 -04:00
Charlie Marsh
90f9e60517 Implement F722 (#175) 2022-09-12 21:34:27 -04:00
Brian Okken
320737f6e4 Change URL to comply with PEP 621 (#173) 2022-09-12 18:38:20 -04:00
Charlie Marsh
dfba1416b2 Implement F406 (#172) 2022-09-12 16:47:30 -04:00
Charlie Marsh
2ca3f35bd1 Remove one match from checks.rs 2022-09-12 16:32:27 -04:00
Adrian Garcia Badaracco
b1c40d5fa7 Run MacOS builds in parallel (#171) 2022-09-12 16:24:13 -04:00
Charlie Marsh
a129e27b3e Tweak rule counts 2022-09-12 15:26:21 -04:00
Charlie Marsh
ad7daa008e Update README to enumerate missing Flake8 rules 2022-09-12 15:24:32 -04:00
Charlie Marsh
062d7081a0 Bump version to 0.0.36 2022-09-12 11:16:26 -04:00
Charlie Marsh
40c1e7e005 Implement F701 and F702 (#169) 2022-09-12 11:16:08 -04:00
Charlie Marsh
3cbd05ddff Update README 2022-09-12 09:31:16 -04:00
Harutaka Kawamura
9414090617 Implement E722 (#166) 2022-09-12 09:04:39 -04:00
Harutaka Kawamura
825777edc1 Add test case for assignment expression in E741.py (#168) 2022-09-12 09:03:36 -04:00
Charlie Marsh
4e0807e908 Include line length in E501 messages (#165) 2022-09-11 22:54:11 -04:00
Charlie Marsh
546be5692a Bump version to 0.0.35 2022-09-11 21:54:00 -04:00
Charlie Marsh
43e1f20b28 Allow unused assignments in for loops and unpacking (#163) 2022-09-11 21:53:45 -04:00
Harutaka Kawamura
97388cefda Implement E743 (#162) 2022-09-11 21:27:33 -04:00
Harutaka Kawamura
63ce579989 Implement E742 (#160) 2022-09-11 20:27:48 -04:00
Charlie Marsh
5f4a62aa40 Bump version to 0.0.34 2022-09-11 18:05:52 -04:00
Charlie Marsh
02ab52b3e2 Implement F407 (#158) 2022-09-11 18:05:28 -04:00
Charlie Marsh
549732b1da Implement F404 (#159) 2022-09-11 18:05:00 -04:00
Harutaka Kawamura
c4565fe0f5 Implement E741 (#137) 2022-09-11 12:30:28 -04:00
Charlie Marsh
81ae3bfc94 Bump version to 0.0.33 2022-09-11 10:45:02 -04:00
Charlie Marsh
62e6feadc7 Handle accesses within inner functions (#156) 2022-09-11 10:44:27 -04:00
Charlie Marsh
18a26e8f0b Allow setting --exclude on the command-line (#157) 2022-09-11 10:44:23 -04:00
Charlie Marsh
2371de3895 Make late imports more permissive (#155) 2022-09-11 10:44:06 -04:00
Charlie Marsh
e3c8f61340 Ignore deletes in conditional branches (#154) 2022-09-11 10:28:07 -04:00
Harutaka Kawamura
f6628ae100 Fix Message.cmp (#152) 2022-09-11 10:18:27 -04:00
Jakub Kuczys
989ed9c10b Fix ruff's pyproject.toml section name in README.md (#148) 2022-09-11 10:18:19 -04:00
Charlie Marsh
8698c06c36 Bump version to 0.0.32 2022-09-10 15:21:01 -04:00
Charlie Marsh
dfd8a4158d Parse function annotations within the ClassDef scope (#144) 2022-09-10 15:20:39 -04:00
Charlie Marsh
c247730bf5 Avoid treating keys as annotations in TypedDict 2022-09-10 15:19:11 -04:00
Charlie Marsh
024472d578 Implement F621 and F622 (#143) 2022-09-10 15:04:33 -04:00
Charlie Marsh
7d69a153e8 Support remaining typing module members (#141) 2022-09-10 14:51:43 -04:00
Charlie Marsh
4fc68e0310 Bump version to 0.0.31 2022-09-10 13:05:15 -04:00
Charlie Marsh
6a24351202 Add support for TypedDict 2022-09-10 13:03:29 -04:00
Charlie Marsh
d7f95ac6b6 Upgrade RustPython parser to handle list assignments 2022-09-10 12:53:07 -04:00
Charlie Marsh
11234ea555 Add await to YieldOutsideFunction 2022-09-08 22:54:11 -04:00
Charlie Marsh
b536159541 Pull check logic out of check_ast.rs (#135) 2022-09-08 22:46:42 -04:00
Charlie Marsh
7c17785eac Bump version to 0.0.30 2022-09-08 11:42:45 -04:00
Charlie Marsh
f1acd28f08 Skip slice error for invalid TypeVar calls 2022-09-08 11:40:55 -04:00
Charlie Marsh
c61ff9a947 Adjust location when parsing deferred type annotations (#133) 2022-09-08 11:37:19 -04:00
Charlie Marsh
2c64cf3149 Add support for Literal, Type, and TypeVar (#131) 2022-09-08 11:07:45 -04:00
Charlie Marsh
fc5f34c76f Use scope-tracking logic for parents (#130) 2022-09-08 09:14:58 -04:00
Charlie Marsh
a8f4faa6e4 Fix crash on missing parent 2022-09-07 22:40:25 -04:00
Charlie Marsh
2ac5c830c1 Parse assignment annotations prior to targets (#127) 2022-09-07 22:35:39 -04:00
Charlie Marsh
994f12050d Support 'ignore' in pyproject.toml (#126) 2022-09-07 22:34:51 -04:00
Charlie Marsh
fad4e4c51d Defer checking of function bodies (#125) 2022-09-07 22:34:43 -04:00
Charlie Marsh
c0042a3ca4 Use Mode::None for --no-cache 2022-09-07 21:47:07 -04:00
Charlie Marsh
5deb63a05f Implement F601 and F602 (#122) 2022-09-07 12:57:50 -04:00
Charlie Marsh
5e9ea8bda2 Add documentation on parity with Flake8 2022-09-07 10:32:28 -04:00
Charlie Marsh
55d1f34bae Bump version to 0.0.29 2022-09-06 22:14:12 -04:00
Charlie Marsh
59b518a54a Upgrade RustPython to handle AnnAssign (#117) 2022-09-06 20:53:51 -04:00
Colin J. Fuller
74ecdc73ac Handle E731 in type-annotated assignment (#116) 2022-09-06 20:18:46 -04:00
Colin J. Fuller
1ad6be7196 Add fixture examples for #114 (#115) 2022-09-06 20:18:05 -04:00
Charlie Marsh
b44d6c2c44 Bump version to 0.0.28 2022-09-06 14:20:02 -04:00
Charlie Marsh
2749660b1f Disable update-informer on linux-cross (#113) 2022-09-06 14:19:38 -04:00
Charlie Marsh
c1eeae90f1 Bump version to 0.0.27 2022-09-06 10:23:48 -04:00
Charlie Marsh
27025055ee Implement E713 and E714 (#111) 2022-09-06 10:23:20 -04:00
Charlie Marsh
e306fe0765 Implement E711 and E712 (#110) 2022-09-06 10:14:36 -04:00
Harutaka Kawamura
5ffb9c08d5 Implement E731 (#109) 2022-09-06 09:48:51 -04:00
Charlie Marsh
1a8940f015 Implement E902 (IOError) (#107) 2022-09-05 13:15:12 -04:00
111 changed files with 7124 additions and 1964 deletions

View File

@@ -1,6 +1,8 @@
name: Release
on:
pull_request:
branches: [main]
create:
tags:
- v*
@@ -14,7 +16,7 @@ env:
PYTHON_VERSION: "3.7" # to build abi3 wheels
jobs:
macos:
macos-x86_64:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
@@ -37,6 +39,25 @@ jobs:
- name: Install built wheel - x86_64
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
with:
name: wheels
path: dist
macos-universal:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
default: true
- name: Build wheels - universal2
uses: messense/maturin-action@v1
with:
@@ -127,7 +148,7 @@ jobs:
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist
args: --no-default-features --release --out dist
maturin-version: "v0.13.0"
- uses: uraimo/run-on-arch-action@v2.0.5
if: matrix.target != 'ppc64'
@@ -259,7 +280,8 @@ jobs:
name: Release
runs-on: ubuntu-latest
needs:
- macos
- macos-universal
- macos-x86_64
- windows
- linux
- linux-cross

View File

@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.26
- 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

93
Cargo.lock generated
View File

@@ -458,6 +458,19 @@ dependencies = [
"cache-padded",
]
[[package]]
name = "console"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"terminal_size",
"winapi 0.3.9",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
@@ -652,6 +665,12 @@ dependencies = [
"log",
]
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "event-listener"
version = "2.5.3"
@@ -1011,6 +1030,20 @@ dependencies = [
"libc",
]
[[package]]
name = "insta"
version = "1.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc61e98be01e89296f3343a878e9f8ca75a494cb5aaf29df65ef55734aeb85f5"
dependencies = [
"console",
"linked-hash-map",
"once_cell",
"serde",
"similar",
"yaml-rust",
]
[[package]]
name = "instant"
version = "0.1.12"
@@ -1122,6 +1155,12 @@ version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "lock_api"
version = "0.4.7"
@@ -1393,6 +1432,24 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "path-absolutize"
version = "3.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3de4b40bd9736640f14c438304c09538159802388febb02c8abaae0846c1f13"
dependencies = [
"path-dedot",
]
[[package]]
name = "path-dedot"
version = "3.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d611d5291372b3738a34ebf0d1f849e58b1dcc1101032f76a346eaa1f8ddbb5b"
dependencies = [
"once_cell",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
@@ -1744,7 +1801,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.26"
version = "0.0.45"
dependencies = [
"anyhow",
"bincode",
@@ -1758,9 +1815,12 @@ dependencies = [
"fern",
"filetime",
"glob",
"insta",
"itertools",
"log",
"notify",
"once_cell",
"path-absolutize",
"rayon",
"regex",
"rustpython-parser",
@@ -1786,7 +1846,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=ff6112d7c70729be76eaad8c9e5b8ef24d13de99#ff6112d7c70729be76eaad8c9e5b8ef24d13de99"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=966a80597d626a9a47eaec78471164422d341453#966a80597d626a9a47eaec78471164422d341453"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -1795,7 +1855,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=ff6112d7c70729be76eaad8c9e5b8ef24d13de99#ff6112d7c70729be76eaad8c9e5b8ef24d13de99"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=966a80597d626a9a47eaec78471164422d341453#966a80597d626a9a47eaec78471164422d341453"
dependencies = [
"bincode",
"bitflags",
@@ -1812,7 +1872,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=ff6112d7c70729be76eaad8c9e5b8ef24d13de99#ff6112d7c70729be76eaad8c9e5b8ef24d13de99"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=966a80597d626a9a47eaec78471164422d341453#966a80597d626a9a47eaec78471164422d341453"
dependencies = [
"ahash",
"anyhow",
@@ -1976,6 +2036,12 @@ dependencies = [
"libc",
]
[[package]]
name = "similar"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803"
[[package]]
name = "siphasher"
version = "0.3.10"
@@ -2098,6 +2164,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]]
name = "terminfo"
version = "0.7.3"
@@ -2593,3 +2669,12 @@ dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.26"
version = "0.0.45"
edition = "2021"
[lib]
@@ -19,20 +19,35 @@ dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.17" }
glob = { version = "0.3.0" }
itertools = { version = "0.10.3" }
log = { version = "0.4.17" }
notify = { version = "4.0.17" }
once_cell = { version = "1.13.1" }
path-absolutize = { version = "3.0.13", features = ["once_cell_cache"] }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "ff6112d7c70729be76eaad8c9e5b8ef24d13de99" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "966a80597d626a9a47eaec78471164422d341453" }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" }
toml = { version = "0.5.9" }
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"] }
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"
codegen-units = 1
opt-level = 3
[profile.dev.package.insta]
opt-level = 3
[profile.dev.package.similar]
opt-level = 3

265
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.26
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.26)
ruff (v0.0.45) 0.0.45
An extremely fast Python linter.
USAGE:
@@ -96,40 +96,163 @@ ARGS:
<FILES>...
OPTIONS:
-e, --exit-zero Exit with status code "0", even upon detecting errors
-f, --fix Attempt to automatically fix lint errors
-h, --help Print help information
--ignore <IGNORE>... Comma-separated list of error codes to ignore
-n, --no-cache Disable cache reads
-q, --quiet Disable all logging (but still exit with status code "1" upon
detecting errors)
--select <SELECT>... Comma-separated list of error codes to enable
-v, --verbose Enable verbose logging
-w, --watch Run in watch mode by re-running whenever files change
--select <SELECT>...
List of error codes to enable
--extend-select <EXTEND_SELECT>...
Like --select, but adds additional error codes on top of the selected ones
--ignore <IGNORE>...
List of error codes to ignore
--extend-ignore <EXTEND_IGNORE>...
Like --ignore, but adds additional error codes on top of the ignored ones
--exclude <EXCLUDE>...
List of paths, used to exclude files and/or directories from checks
--extend-exclude <EXTEND_EXCLUDE>...
Like --exclude, but adds additional files and directories on top of the excluded ones
-e, --exit-zero
Exit with status code "0", even upon detecting errors
-f, --fix
Attempt to automatically fix lint errors
--format <FORMAT>
Output serialization format for error messages [default: text] [possible values: text,
json]
-h, --help
Print help information
-n, --no-cache
Disable cache reads
-q, --quiet
Disable all logging (but still exit with status code "1" upon detecting errors)
--add-noqa
Enable automatic additions of noqa directives to failing lines
--show-files
See the files ruff will be run against with the current settings
--show-settings
See ruff's settings
-v, --verbose
Enable verbose logging
-V, --version
Print version information
-w, --watch
Run in watch mode by re-running whenever files change
```
### Excluding files
Exclusions are based on globs, and can be either:
- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the
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
project root (e.g., the directory containing your `pyproject.toml`).
### Ignoring errors
To omit a lint check entirely, add it to the "ignore" list via `--ignore` or `--extend-ignore`,
either on the command-line or in your `project.toml` file.
To ignore an error in-line, ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
To ignore an individual error, add `# noqa: {code}` to the end of the line, like so:
```python
# Ignore F841.
x = 1 # noqa: F841
# Ignore E741 and F841.
i = 1 # noqa: E741, F841
# Ignore _all_ errors.
x = 1 # noqa
```
Note that, for multi-line strings, the `noqa` directive should come at the end of the string, and
will apply to the entire body, like so:
```python
"""Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa: E501
```
ruff supports several (experimental) workflows to aid in `noqa` management.
First, ruff provides a special error code, `M001`, to enforce that your `noqa` directives are
"valid", in that the errors they _say_ they ignore are actually being triggered on that line (and
thus suppressed). **You can run `ruff /path/to/file.py --extend-select M001` to flag unused `noqa`
directives.**
Second, ruff can _automatically remove_ unused `noqa` directives via its autofix functionality.
**You can run `ruff /path/to/file.py --extend-select M001 --fix` to automatically remove unused
`noqa` directives.**
Third, ruff can _automatically add_ `noqa` directives to all failing lines. This is useful when
migrating a new codebase to ruff. **You can run `ruff /path/to/file.py --add-noqa` to automatically
add `noqa` directives to all failing lines, with the appropriate error codes.**
### Compatibility with Black
ruff is intended to be compatible with [Black](https://github.com/psf/black), and should be
compatible out-of-the-box as long as the `line-length` setting is consistent between the two.
ruff is compatible with [Black](https://github.com/psf/black) out-of-the-box, as long as
the `line-length` setting is consistent between the two.
As a project, ruff is designed to be used alongside Black and, as such, will defer implementing
lint rules that are obviated by Black (e.g., stylistic rules).
stylistic lint rules that are obviated by autoformatting.
### Parity with Flake8
ruff's goal is to achieve feature-parity with Flake8 when used (1) without plugins, (2) alongside
Black, and (3) on Python 3 code.
**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 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
| Code | Name | Message |
| ---- | ----- | ------- |
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file |
| E501 | LineTooLong | Line too long |
| E501 | LineTooLong | Line too long (89 > 88 characters) |
| E711 | NoneComparison | Comparison to `None` should be `cond is None` |
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` |
| E713 | NotInTest | Test for membership should be `not in` |
| E714 | NotIsTest | Test for object identity should be `is not` |
| E721 | TypeComparison | do not compare types, use `isinstance()` |
| E722 | DoNotUseBareExcept | Do not use bare `except` |
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def |
| E741 | AmbiguousVariableName | ambiguous variable name '...' |
| E742 | AmbiguousClassName | ambiguous class name '...' |
| E743 | AmbiguousFunctionName | ambiguous function name '...' |
| E902 | IOError | ... |
| E999 | SyntaxError | SyntaxError: ... |
| F401 | UnusedImport | `...` imported but unused |
| F403 | ImportStarUsage | 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 |
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated |
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated |
| F621 | TooManyExpressionsInStarredAssignment | too many expressions in star-unpacking assignment |
| F622 | TwoStarredExpressions | two starred expressions in assignment |
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` |
| F632 | IsLiteral | use ==/!= to compare constant literals |
| F633 | InvalidPrintSyntax | use of >> is invalid with print function |
| F634 | IfTuple | If test is a tuple, which is always `True` |
| F701 | BreakOutsideLoop | `break` outside loop |
| F702 | ContinueOutsideLoop | `continue` not properly in loop |
| F704 | YieldOutsideFunction | a `yield` or `yield from` statement outside of a function/method |
| F706 | ReturnOutsideFunction | a `return` statement outside of a function/method |
| F707 | DefaultExceptNotLast | an `except:` block as not the last exception handler |
| F722 | ForwardAnnotationSyntaxError | syntax error in forward annotation '...' |
| F821 | UndefinedName | Undefined name `...` |
| F822 | UndefinedExport | Undefined name `...` in `__all__` |
| F823 | UndefinedLocal | Local variable `...` referenced before assignment |
@@ -138,6 +261,7 @@ lint rules that are obviated by Black (e.g., stylistic rules).
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` |
| R001 | UselessObjectInheritance | Class `...` inherits from object |
| R002 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead |
| M001 | UnusedNOQA | Unused `noqa` directive |
## Development
@@ -171,9 +295,9 @@ git clone --branch 3.10 https://github.com/python/cpython.git resources/test/cpy
Add this `pyproject.toml` to the CPython directory:
```toml
[tool.linter]
[tool.ruff]
line-length = 88
exclude = [
extend-exclude = [
"Lib/lib2to3/tests/data/bom.py",
"Lib/lib2to3/tests/data/crlf.py",
"Lib/lib2to3/tests/data/different_encoding.py",
@@ -204,17 +328,21 @@ Next, to benchmark the release build:
```shell
cargo build --release
hyperfine --ignore-failure --warmup 1 \
hyperfine --ignore-failure --warmup 10 --runs 100 \
"./target/release/ruff ./resources/test/cpython/ --no-cache" \
"./target/release/ruff ./resources/test/cpython/"
Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
Time (mean ± σ): 353.6 ms ± 7.6 ms [User: 2868.8 ms, System: 171.5 ms]
Range (min … max): 344.4 ms … 367.3 ms 10 runs
Time (mean ± σ): 297.4 ms ± 4.9 ms [User: 2460.0 ms, System: 67.2 ms]
Range (min … max): 287.7 ms … 312.1 ms 100 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: ./target/release/ruff ./resources/test/cpython/
Time (mean ± σ): 59.6 ms ± 2.5 ms [User: 36.4 ms, System: 345.6 ms]
Range (min … max): 55.9 ms … 67.0 ms 48 runs
Time (mean ± σ): 79.6 ms ± 7.3 ms [User: 59.7 ms, System: 356.1 ms]
Range (min … max): 62.4 ms … 111.2 ms 100 runs
Warning: Ignoring non-zero exit code.
```
To benchmark against the ecosystem's existing tools:
@@ -226,11 +354,8 @@ hyperfine --ignore-failure --warmup 5 \
"pyflakes resources/test/cpython" \
"autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython" \
"pycodestyle resources/test/cpython" \
"pycodestyle --select E501 resources/test/cpython" \
"flake8 resources/test/cpython" \
"flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython" \
"python -m scripts.run_flake8 resources/test/cpython" \
"python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501"
"python -m scripts.run_flake8 resources/test/cpython"
```
In order, these evaluate:
@@ -240,66 +365,58 @@ In order, these evaluate:
- PyFlakes
- autoflake
- pycodestyle
- pycodestyle, limited to the checks supported by ruff
- Flake8
- Flake8, limited to the checks supported by ruff
- Flake8, with a hack to enable multiprocessing on macOS
- Flake8, with a hack to enable multiprocessing on macOS, limited to the checks supported by ruff
(You can `poetry install` from `./scripts` to create a working environment for the above.)
```shell
Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
Time (mean ± σ): 469.3 ms ± 16.3 ms [User: 2663.0 ms, System: 972.5 ms]
Range (min … max): 445.2 ms … 494.8 ms 10 runs
Time (mean ± σ): 297.9 ms ± 7.0 ms [User: 2436.6 ms, System: 65.9 ms]
Range (min … max): 289.9 ms … 314.6 ms 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: pylint --recursive=y resources/test/cpython/
Time (mean ± σ): 27.211 s ± 0.097 s [User: 26.405 s, System: 0.799 s]
Range (min … max): 27.056 s … 27.349 s 10 runs
Time (mean ± σ): 37.634 s ± 0.225 s [User: 36.728 s, System: 0.853 s]
Range (min … max): 37.201 s … 38.106 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 3: pyflakes resources/test/cpython
Time (mean ± σ): 27.309 s ± 0.033 s [User: 27.137 s, System: 0.169 s]
Range (min … max): 27.267 s … 27.372 s 10 runs
Time (mean ± σ): 40.950 s ± 0.449 s [User: 40.688 s, System: 0.229 s]
Range (min … max): 40.348 s … 41.671 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 4: autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython
Time (mean ± σ): 8.027 s ± 0.024 s [User: 74.255 s, System: 0.953 s]
Range (min … max): 7.969 s … 8.052 s 10 runs
Time (mean ± σ): 11.562 s ± 0.160 s [User: 107.022 s, System: 1.143 s]
Range (min … max): 11.417 s … 11.917 s 10 runs
Benchmark 5: pycodestyle resources/test/cpython
Time (mean ± σ): 41.666 s ± 0.266 s [User: 41.531 s, System: 0.132 s]
Range (min … max): 41.295 s … 41.980 s 10 runs
Time (mean ± σ): 67.428 s ± 0.985 s [User: 67.199 s, System: 0.203 s]
Range (min … max): 65.313 s … 68.496 s 10 runs
Benchmark 6: pycodestyle --select E501 resources/test/cpython
Time (mean ± σ): 14.547 s ± 0.077 s [User: 14.466 s, System: 0.079 s]
Range (min … max): 14.429 s … 14.695 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 7: flake8 resources/test/cpython
Time (mean ± σ): 75.700 s ± 0.152 s [User: 75.254 s, System: 0.440 s]
Range (min … max): 75.513 s … 76.014 s 10 runs
Benchmark 6: flake8 resources/test/cpython
Time (mean ± σ): 116.099 s ± 1.178 s [User: 115.217 s, System: 0.845 s]
Range (min … max): 114.180 s … 117.724 s 10 runs
Benchmark 8: flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython
Time (mean ± σ): 75.122 s ± 0.532 s [User: 74.677 s, System: 0.440 s]
Range (min … max): 74.130 s … 75.606 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 9: python -m scripts.run_flake8 resources/test/cpython
Time (mean ± σ): 12.794 s ± 0.147 s [User: 90.792 s, System: 0.738 s]
Range (min … max): 12.606 s … 13.030 s 10 runs
Benchmark 10: python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501
Time (mean ± σ): 12.487 s ± 0.118 s [User: 90.052 s, System: 0.714 s]
Range (min … max): 12.265 s … 12.665 s 10 runs
Benchmark 7: python -m scripts.run_flake8 resources/test/cpython
Time (mean ± σ): 20.477 s ± 0.349 s [User: 142.372 s, System: 1.504 s]
Range (min … max): 20.107 s … 21.183 s 10 runs
Summary
'./target/release/ruff ./resources/test/cpython/ --no-cache' ran
17.10 ± 0.60 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
26.60 ± 0.96 times faster than 'python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501'
27.26 ± 1.00 times faster than 'python -m scripts.run_flake8 resources/test/cpython'
30.99 ± 1.09 times faster than 'pycodestyle --select E501 resources/test/cpython'
57.98 ± 2.03 times faster than 'pylint --recursive=y resources/test/cpython/'
58.19 ± 2.02 times faster than 'pyflakes resources/test/cpython'
88.77 ± 3.14 times faster than 'pycodestyle resources/test/cpython'
160.06 ± 5.68 times faster than 'flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython'
161.29 ± 5.61 times faster than 'flake8 resources/test/cpython'
38.81 ± 1.05 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
68.74 ± 1.99 times faster than 'python -m scripts.run_flake8 resources/test/cpython'
126.33 ± 3.05 times faster than 'pylint --recursive=y resources/test/cpython/'
137.46 ± 3.55 times faster than 'pyflakes resources/test/cpython'
226.35 ± 6.23 times faster than 'pycodestyle resources/test/cpython'
389.73 ± 9.92 times faster than 'flake8 resources/test/cpython'
```
## License

View File

@@ -1,32 +1,14 @@
/// Generate a Markdown-compatible table of supported lint rules.
use ruff::checks::CheckKind;
use ruff::checks::{CheckCode, ALL_CHECK_CODES};
fn main() {
let mut check_kinds: Vec<CheckKind> = vec![
CheckKind::AssertTuple,
CheckKind::DefaultExceptNotLast,
CheckKind::DuplicateArgumentName,
CheckKind::FStringMissingPlaceholders,
CheckKind::IfTuple,
CheckKind::ImportStarUsage,
CheckKind::LineTooLong,
CheckKind::ModuleImportNotAtTopOfFile,
CheckKind::NoAssertEquals,
CheckKind::RaiseNotImplemented,
CheckKind::ReturnOutsideFunction,
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(),

View File

@@ -19,10 +19,12 @@ classifiers = [
]
author = "Charlie Marsh"
author_email = "charlie.r.marsh@gmail.com"
url = "https://github.com/charliermarsh/ruff"
description = "An extremely fast Python linter, written in Rust."
requires-python = ">=3.7"
[project.urls]
repository = "https://github.com/charliermarsh/ruff"
[build-system]
requires = ["maturin>=0.13,<0.14"]
build-backend = "maturin"

View File

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

50
resources/test/fixtures/E711.py vendored Normal file
View File

@@ -0,0 +1,50 @@
#: E711
if res == None:
pass
#: E711
if res != None:
pass
#: E711
if None == res:
pass
#: E711
if None != res:
pass
#: E711
if res[1] == None:
pass
#: E711
if res[1] != None:
pass
#: E711
if None != res[1]:
pass
#: E711
if None == res[1]:
pass
#: Okay
if x not in y:
pass
if not (X in Y or X is Z):
pass
if not (X in Y):
pass
if x is not y:
pass
if X is not Y is not Z:
pass
if TrueElement.get_element(True) == TrueElement.get_element(False):
pass
if (True) == TrueElement or x == TrueElement:
pass
assert (not foo) in bar
assert {"x": not foo} in bar
assert [42, not foo] in bar

46
resources/test/fixtures/E712.py vendored Normal file
View File

@@ -0,0 +1,46 @@
#: E712
if res == True:
pass
#: E712
if res != False:
pass
#: E712
if True != res:
pass
#: E712
if False == res:
pass
#: E712
if res[1] == True:
pass
#: E712
if res[1] != False:
pass
#: E712
var = 1 if cond == True else -1 if cond == False else cond
#: E712
if (True) == TrueElement or x == TrueElement:
pass
#: Okay
if x not in y:
pass
if not (X in Y or X is Z):
pass
if not (X in Y):
pass
if x is not y:
pass
if X is not Y is not Z:
pass
if TrueElement.get_element(True) == TrueElement.get_element(False):
pass
assert (not foo) in bar
assert {"x": not foo} in bar
assert [42, not foo] in bar

38
resources/test/fixtures/E713.py vendored Normal file
View File

@@ -0,0 +1,38 @@
#: E713
if not X in Y:
pass
#: E713
if not X.B in Y:
pass
#: E713
if not X in Y and Z == "zero":
pass
#: E713
if X == "zero" or not Y in Z:
pass
#: E713
if not (X in Y):
pass
#: Okay
if x not in y:
pass
if not (X in Y or X is Z):
pass
if x is not y:
pass
if X is not Y is not Z:
pass
if TrueElement.get_element(True) == TrueElement.get_element(False):
pass
if (True) == TrueElement or x == TrueElement:
pass
assert (not foo) in bar
assert {"x": not foo} in bar
assert [42, not foo] in bar

38
resources/test/fixtures/E714.py vendored Normal file
View File

@@ -0,0 +1,38 @@
#: E714
if not X is Y:
pass
#: E714
if not X.B is Y:
pass
#: E714
if not X is Y is not Z:
pass
#: Okay
if not X is not Y:
pass
if x not in y:
pass
if not (X in Y or X is Z):
pass
if not (X in Y):
pass
if x is not y:
pass
if X is not Y is not Z:
pass
if TrueElement.get_element(True) == TrueElement.get_element(False):
pass
if (True) == TrueElement or x == TrueElement:
pass
assert (not foo) in bar
assert {"x": not foo} in bar
assert [42, not foo] in bar

54
resources/test/fixtures/E721.py vendored Normal file
View File

@@ -0,0 +1,54 @@
#: 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,))
#: E211 E721
assert type(res) == type(
[
2,
]
)
#: E201 E201 E202 E721
assert type(res) == type(())
#: E201 E202 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

33
resources/test/fixtures/E722.py vendored Normal file
View File

@@ -0,0 +1,33 @@
#: E722
try:
pass
except:
pass
#: E722
try:
pass
except Exception:
pass
except:
pass
#: E722
try:
pass
except:
pass
#: Okay
fake_code = """"
try:
do_something()
except:
pass
"""
try:
pass
except Exception:
pass
#: Okay
from . import compute_type
if compute_type(foo) == 5:
pass

21
resources/test/fixtures/E731.py vendored Normal file
View File

@@ -0,0 +1,21 @@
#: E731
f = lambda x: 2 * x
#: E731
f = lambda x: 2 * x
#: E731
while False:
this = lambda y, z: 2 * x
f = object()
#: E731
f.method = lambda: "Method"
f = {}
#: E731
f["a"] = lambda x: x ** 2
f = []
f.append(lambda x: x ** 2)
lambda: "no-op"

75
resources/test/fixtures/E741.py vendored Normal file
View File

@@ -0,0 +1,75 @@
from contextlib import contextmanager
l = 0
I = 0
O = 0
l: int = 0
a, l = 0, 1
[a, l] = 0, 1
a, *l = 0, 1, 2
a = l = 0
o = 0
i = 0
for l in range(3):
pass
for a, l in zip(range(3), range(3)):
pass
def f1():
global l
l = 0
def f2():
l = 0
def f3():
nonlocal l
l = 1
f3()
return l
def f4(l, /, I):
return l, I, O
def f5(l=0, *, I=1):
return l, I
def f6(*l, **I):
return l, I
@contextmanager
def ctx1():
yield 0
with ctx1() as l:
pass
@contextmanager
def ctx2():
yield 0, 1
with ctx2() as (a, l):
pass
try:
pass
except ValueError as l:
pass
if (l := 5) > 0:
pass

14
resources/test/fixtures/E742.py vendored Normal file
View File

@@ -0,0 +1,14 @@
class l:
pass
class I:
pass
class O:
pass
class X:
pass

15
resources/test/fixtures/E743.py vendored Normal file
View File

@@ -0,0 +1,15 @@
def l():
pass
def I():
pass
class X:
def O(self):
pass
def x():
pass

2
resources/test/fixtures/E999.py vendored Normal file
View File

@@ -0,0 +1,2 @@
def x():

View File

@@ -1,6 +1,7 @@
from __future__ import all_feature_names
import os
import functools
from datetime import datetime
from collections import (
Counter,
OrderedDict,
@@ -10,11 +11,18 @@ import multiprocessing.pool
import multiprocessing.process
import logging.config
import logging.handlers
from typing import TYPING_CHECK, NamedTuple, Dict, Type, TypeVar, List, Set, Union, cast
from blah import ClassA, ClassB, ClassC
if TYPING_CHECK:
from models import Fruit, Nut, Vegetable
class X:
datetime: datetime
foo: Type["NamedTuple"]
def a(self) -> "namedtuple":
x = os.environ["1"]
y = Counter()
@@ -23,3 +31,11 @@ class X:
__all__ = ["ClassA"] + ["ClassB"]
__all__ += ["ClassC"]
X = TypeVar("X")
Y = TypeVar("Y", bound="Dict")
Z = TypeVar("Z", "List", "Set")
a = list["Fruit"]
b = Union["Nut", None]
c = cast("Vegetable", b)

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

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

@@ -0,0 +1,7 @@
from __future__ import print_function
"""Docstring"""
from __future__ import absolute_import
from collections import namedtuple
from __future__ import print_function

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

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

@@ -0,0 +1,9 @@
from F634 import *
def f():
from F634 import *
class F:
from F634 import *

2
resources/test/fixtures/F407.py vendored Normal file
View File

@@ -0,0 +1,2 @@
from __future__ import print_function
from __future__ import non_existent_feature

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

@@ -0,0 +1,12 @@
x = {
"a": 1,
"a": 2,
"b": 3,
("a", "b"): 3,
("a", "b"): 4,
1.0: 2,
1: 0,
1: 3,
b"123": 1,
b"123": 4,
}

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

@@ -0,0 +1,7 @@
a = 1
b = 2
x = {
a: 1,
a: 2,
b: 3,
}

3
resources/test/fixtures/F622.py vendored Normal file
View File

@@ -0,0 +1,3 @@
*a, *b, c = (1, 2, 3)
*a, b, c = (1, 2, 3)
a, b, *c = (1, 2, 3)

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

@@ -0,0 +1,5 @@
if x is "abc":
pass
if 123 is not y:
pass

4
resources/test/fixtures/F633.py vendored Normal file
View File

@@ -0,0 +1,4 @@
from __future__ import print_function
import sys
print >> sys.stderr, "Hello"

23
resources/test/fixtures/F701.py vendored Normal file
View File

@@ -0,0 +1,23 @@
for i in range(10):
break
else:
break
i = 0
while i < 10:
i += 1
break
def f():
for i in range(10):
break
break
class Foo:
break
break

23
resources/test/fixtures/F702.py vendored Normal file
View File

@@ -0,0 +1,23 @@
for i in range(10):
continue
else:
continue
i = 0
while i < 10:
i += 1
continue
def f():
for i in range(10):
continue
continue
class Foo:
continue
continue

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

@@ -0,0 +1,10 @@
class A:
pass
def f() -> "A":
pass
def g() -> "///":
pass

View File

@@ -33,14 +33,59 @@ def ternary_optarg(prec, exp_range, itr):
class Foo:
CLASS_VAR = 1
REFERENCES_CLASS_VAR = {"CLASS_VAR": CLASS_VAR}
ANNOTATED_CLASS_VAR: int = 2
from typing import Literal
class Class:
copy_on_model_validation: Literal["none", "deep", "shallow"]
post_init_call: Literal["before_validation", "after_validation"]
def __init__(self):
# TODO(charlie): This should be recognized as a defined variable.
Class # noqa: F821
Class
try:
x = 1 / 0
except Exception as e:
print(e)
y: int = 1
x: "Bar" = 1
[first] = ["yup"]
from typing import List, TypedDict
class Item(TypedDict):
nodes: List[TypedDict("Node", {"name": str})]
from enum import Enum
class Ticket:
class Status(Enum):
OPEN = "OPEN"
CLOSED = "CLOSED"
def set_status(self, status: Status):
self.status = status
def update_tomato():
print(TOMATO)
TOMATO = "cherry tomato"
A = f'{B}'
A = (
f'B'
f'{B}'
)

View File

@@ -14,3 +14,18 @@ def f():
x = 1
y = 2
z = x + y
def g():
foo = (1, 2)
(a, b) = (1, 2)
bar = (1, 2)
(c, d) = bar
(x, y) = baz = bar
def h():
locals()
x = 1

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

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

View File

@@ -0,0 +1,3 @@
print(__path__)
__all__ = ["a", "b", "c"]

View File

@@ -0,0 +1,9 @@
a = "abc"
b = f"ghi{'jkl'}"
c = f"def"
d = f"def" + "ghi"
e = (
f"def" +
"ghi"
)

View File

@@ -0,0 +1,27 @@
from __future__ import annotations
from dataclasses import dataclass
from models import Fruit, Nut
@dataclass
class Foo:
x: int
y: int
@classmethod
def a(cls) -> Foo:
return cls(x=0, y=0)
@classmethod
def b(cls) -> "Foo":
return cls(x=0, y=0)
@classmethod
def c(cls) -> Bar:
return cls(x=0, y=0)
@classmethod
def d(cls) -> Fruit:
return cls(x=0, y=0)

View File

@@ -1,23 +1,7 @@
[tool.ruff]
line-length = 88
exclude = ["excluded.py", "**/migrations"]
select = [
"E402",
"E501",
"F401",
"F403",
"F541",
"F631",
"F634",
"F704",
"F706",
"F707",
"F821",
"F822",
"F823",
"F831",
"F841",
"F901",
"R001",
"R002",
extend-exclude = [
"excluded.py",
"migrations",
"directory/also_excluded.py",
]

5
src/ast.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod checks;
pub mod operations;
pub mod relocate;
pub mod types;
pub mod visitor;

650
src/ast/checks.rs Normal file
View File

@@ -0,0 +1,650 @@
use std::collections::BTreeSet;
use itertools::izip;
use rustpython_parser::ast::{
Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword,
Location, Stmt, StmtKind, Unaryop,
};
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::{Binding, BindingKind, CheckLocator, FunctionScope, Scope, ScopeKind};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckKind, Fix, RejectedCmpop};
/// Check IfTuple compliance.
pub fn check_if_tuple(test: &Expr, location: Location) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
return Some(Check::new(CheckKind::IfTuple, location));
}
}
None
}
/// Check AssertTuple compliance.
pub fn check_assert_tuple(test: &Expr, location: Location) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
return Some(Check::new(CheckKind::AssertTuple, location));
}
}
None
}
/// Check NotInTest and NotIsTest compliance.
pub fn check_not_tests(
op: &Unaryop,
operand: &Expr,
check_not_in: bool,
check_not_is: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
if matches!(op, Unaryop::Not) {
if let ExprKind::Compare { ops, .. } = &operand.node {
for op in ops {
match op {
Cmpop::In => {
if check_not_in {
checks.push(Check::new(
CheckKind::NotInTest,
locator.locate_check(operand.location),
));
}
}
Cmpop::Is => {
if check_not_is {
checks.push(Check::new(
CheckKind::NotIsTest,
locator.locate_check(operand.location),
));
}
}
_ => {}
}
}
}
}
checks
}
/// Check UnusedVariable compliance.
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()
&& name != "_"
&& name != "__tracebackhide__"
&& name != "__traceback_info__"
&& name != "__traceback_supplement__"
&& matches!(binding.kind, BindingKind::Assignment)
{
checks.push(Check::new(
CheckKind::UnusedVariable(name.to_string()),
locator.locate_check(binding.location),
));
}
}
checks
}
/// Check DoNotAssignLambda compliance.
pub fn check_do_not_assign_lambda(value: &Expr, location: Location) -> Option<Check> {
if let ExprKind::Lambda { .. } = &value.node {
Some(Check::new(CheckKind::DoNotAssignLambda, location))
} else {
None
}
}
fn is_ambiguous_name(name: &str) -> bool {
name == "l" || name == "I" || name == "O"
}
/// Check AmbiguousVariableName compliance.
pub fn check_ambiguous_variable_name(name: &str, location: Location) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousVariableName(name.to_string()),
location,
))
} else {
None
}
}
/// Check AmbiguousClassName compliance.
pub fn check_ambiguous_class_name(name: &str, location: Location) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousClassName(name.to_string()),
location,
))
} else {
None
}
}
/// Check AmbiguousFunctionName compliance.
pub fn check_ambiguous_function_name(name: &str, location: Location) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousFunctionName(name.to_string()),
location,
))
} else {
None
}
}
/// Check UselessObjectInheritance compliance.
pub fn check_useless_object_inheritance(
stmt: &Stmt,
name: &str,
bases: &[Expr],
keywords: &[Keyword],
scope: &Scope,
locator: &mut SourceCodeLocator,
autofix: &fixer::Mode,
) -> Option<Check> {
for expr in bases {
if let ExprKind::Name { id, .. } = &expr.node {
if id == "object" {
match scope.values.get(id) {
None
| Some(Binding {
kind: BindingKind::Builtin,
..
}) => {
let mut check = Check::new(
CheckKind::UselessObjectInheritance(name.to_string()),
expr.location,
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_class_def_base(
locator,
&stmt.location,
expr.location,
bases,
keywords,
) {
check.amend(fix);
}
}
return Some(check);
}
_ => {}
}
}
}
}
None
}
/// Check DefaultExceptNotLast compliance.
pub fn check_default_except_not_last(handlers: &Vec<Excepthandler>) -> Option<Check> {
for (idx, handler) in handlers.iter().enumerate() {
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
if type_.is_none() && idx < handlers.len() - 1 {
return Some(Check::new(
CheckKind::DefaultExceptNotLast,
handler.location,
));
}
}
None
}
/// Check RaiseNotImplemented compliance.
pub fn check_raise_not_implemented(expr: &Expr) -> Option<Check> {
match &expr.node {
ExprKind::Call { func, .. } => {
if let ExprKind::Name { id, .. } = &func.node {
if id == "NotImplemented" {
return Some(Check::new(CheckKind::RaiseNotImplemented, expr.location));
}
}
}
ExprKind::Name { id, .. } => {
if id == "NotImplemented" {
return Some(Check::new(CheckKind::RaiseNotImplemented, expr.location));
}
}
_ => {}
}
None
}
/// Check DuplicateArgumentName compliance.
pub fn check_duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
// Collect all the arguments into a single vector.
let mut all_arguments: Vec<&Arg> = arguments
.args
.iter()
.chain(arguments.posonlyargs.iter())
.chain(arguments.kwonlyargs.iter())
.collect();
if let Some(arg) = &arguments.vararg {
all_arguments.push(arg);
}
if let Some(arg) = &arguments.kwarg {
all_arguments.push(arg);
}
// Search for duplicates.
let mut idents: BTreeSet<&str> = BTreeSet::new();
for arg in all_arguments {
let ident = &arg.node.arg;
if idents.contains(ident.as_str()) {
checks.push(Check::new(CheckKind::DuplicateArgumentName, arg.location));
}
idents.insert(ident);
}
checks
}
/// Check AssertEquals compliance.
pub fn check_assert_equals(expr: &Expr, autofix: &fixer::Mode) -> Option<Check> {
if let ExprKind::Attribute { value, attr, .. } = &expr.node {
if attr == "assertEquals" {
if let ExprKind::Name { id, .. } = &value.node {
if id == "self" {
let mut check = Check::new(CheckKind::NoAssertEquals, expr.location);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "assertEqual".to_string(),
start: Location::new(expr.location.row(), expr.location.column() + 1),
end: Location::new(
expr.location.row(),
expr.location.column() + 1 + "assertEquals".len(),
),
applied: false,
});
}
return Some(check);
}
}
}
}
None
}
#[derive(Debug, PartialEq)]
enum DictionaryKey<'a> {
Constant(&'a Constant),
Variable(&'a String),
}
fn convert_to_value(expr: &Expr) -> Option<DictionaryKey> {
match &expr.node {
ExprKind::Constant { value, .. } => Some(DictionaryKey::Constant(value)),
ExprKind::Name { id, .. } => Some(DictionaryKey::Variable(id)),
_ => None,
}
}
/// Check MultiValueRepeatedKeyLiteral and MultiValueRepeatedKeyVariable compliance.
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![];
let num_keys = keys.len();
for i in 0..num_keys {
let k1 = &keys[i];
let v1 = convert_to_value(k1);
for k2 in keys.iter().take(num_keys).skip(i + 1) {
let v2 = convert_to_value(k2);
match (&v1, &v2) {
(Some(DictionaryKey::Constant(v1)), Some(DictionaryKey::Constant(v2))) => {
if check_repeated_literals && v1 == v2 {
checks.push(Check::new(
CheckKind::MultiValueRepeatedKeyLiteral,
locator.locate_check(k2.location),
))
}
}
(Some(DictionaryKey::Variable(v1)), Some(DictionaryKey::Variable(v2))) => {
if check_repeated_variables && v1 == v2 {
checks.push(Check::new(
CheckKind::MultiValueRepeatedKeyVariable((*v2).to_string()),
locator.locate_check(k2.location),
))
}
}
_ => {}
}
}
}
checks
}
/// Check TrueFalseComparison and NoneComparison compliance.
pub fn check_literal_comparisons(
left: &Expr,
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
check_none_comparisons: bool,
check_true_false_comparisons: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
let op = ops.first().unwrap();
let comparator = left;
// Check `left`.
if check_none_comparisons
&& matches!(
comparator.node,
ExprKind::Constant {
value: Constant::None,
kind: None
}
)
{
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::Eq),
locator.locate_check(comparator.location),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
locator.locate_check(comparator.location),
));
}
}
if check_true_false_comparisons {
if let ExprKind::Constant {
value: Constant::Bool(value),
kind: None,
} = comparator.node
{
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
locator.locate_check(comparator.location),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
locator.locate_check(comparator.location),
));
}
}
}
// Check each comparator in order.
for (op, comparator) in izip!(ops, comparators) {
if check_none_comparisons
&& matches!(
comparator.node,
ExprKind::Constant {
value: Constant::None,
kind: None
}
)
{
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::Eq),
locator.locate_check(comparator.location),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
locator.locate_check(comparator.location),
));
}
}
if check_true_false_comparisons {
if let ExprKind::Constant {
value: Constant::Bool(value),
kind: None,
} = comparator.node
{
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
locator.locate_check(comparator.location),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
locator.locate_check(comparator.location),
));
}
}
}
}
checks
}
fn is_constant(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Constant { .. } => true,
ExprKind::Tuple { elts, .. } => elts.iter().all(is_constant),
_ => false,
}
}
fn is_singleton(expr: &Expr) -> bool {
matches!(
expr.node,
ExprKind::Constant {
value: Constant::None | Constant::Bool(_) | Constant::Ellipsis,
..
}
)
}
fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// Check IsLiteral compliance.
pub fn check_is_literal(
left: &Expr,
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
location: Location,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
let mut left = left;
for (op, right) in izip!(ops, comparators) {
if matches!(op, Cmpop::Is | Cmpop::IsNot)
&& (is_constant_non_singleton(left) || is_constant_non_singleton(right))
{
checks.push(Check::new(CheckKind::IsLiteral, location));
}
left = right;
}
checks
}
/// Check TypeComparison compliance.
pub fn check_type_comparison(
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
location: Location,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for (op, right) in izip!(ops, comparators) {
if matches!(op, Cmpop::Is | Cmpop::IsNot | Cmpop::Eq | Cmpop::NotEq) {
match &right.node {
ExprKind::Call { func, args, .. } => {
if let ExprKind::Name { id, .. } = &func.node {
// Ex) type(False)
if id == "type" {
if let Some(arg) = args.first() {
// Allow comparison for types which are not obvious.
if !matches!(arg.node, ExprKind::Name { .. }) {
checks.push(Check::new(CheckKind::TypeComparison, location));
}
}
}
}
}
ExprKind::Attribute { value, .. } => {
if let ExprKind::Name { id, .. } = &value.node {
// Ex) types.IntType
if id == "types" {
checks.push(Check::new(CheckKind::TypeComparison, location));
}
}
}
_ => {}
}
}
}
checks
}
/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance.
pub fn check_starred_expressions(
elts: &[Expr],
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;
for (index, elt) in elts.iter().enumerate() {
if matches!(elt.node, ExprKind::Starred { .. }) {
if has_starred && check_two_starred_expressions {
return Some(Check::new(CheckKind::TwoStarredExpressions, location));
}
has_starred = true;
starred_index = Some(index);
}
}
if check_too_many_expressions {
if let Some(starred_index) = starred_index {
if starred_index >= 1 << 8 || elts.len() - starred_index > 1 << 24 {
return Some(Check::new(
CheckKind::TooManyExpressionsInStarredAssignment,
location,
));
}
}
}
None
}
/// Check BreakOutsideLoop compliance.
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;
for index in parent_stack.iter().rev() {
let child = parent;
parent = parents[*index];
match &parent.node {
StmtKind::For { orelse, .. }
| StmtKind::AsyncFor { orelse, .. }
| StmtKind::While { orelse, .. } => {
if !orelse.contains(child) {
allowed = true;
break;
}
}
StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
| StmtKind::ClassDef { .. } => {
break;
}
_ => {}
}
}
if !allowed {
Some(Check::new(
CheckKind::BreakOutsideLoop,
locator.locate_check(stmt.location),
))
} else {
None
}
}
/// Check ContinueOutsideLoop compliance.
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;
for index in parent_stack.iter().rev() {
let child = parent;
parent = parents[*index];
match &parent.node {
StmtKind::For { orelse, .. }
| StmtKind::AsyncFor { orelse, .. }
| StmtKind::While { orelse, .. } => {
if !orelse.contains(child) {
allowed = true;
break;
}
}
StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
| StmtKind::ClassDef { .. } => {
break;
}
_ => {}
}
}
if !allowed {
Some(Check::new(
CheckKind::ContinueOutsideLoop,
locator.locate_check(stmt.location),
))
} else {
None
}
}

View File

@@ -1,56 +1,6 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
fn id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
pub enum ScopeKind {
Class,
Function,
Generator,
Module,
}
pub struct Scope {
pub id: usize,
pub kind: ScopeKind,
pub values: BTreeMap<String, Binding>,
}
impl Scope {
pub fn new(kind: ScopeKind) -> Self {
Scope {
id: id(),
kind,
values: BTreeMap::new(),
}
}
}
#[derive(Clone, Debug)]
pub enum BindingKind {
Argument,
Assignment,
Builtin,
ClassDefinition,
Definition,
Export(Vec<String>),
FutureImportation,
Importation(String),
StarImportation,
SubmoduleImportation(String),
}
#[derive(Clone, Debug)]
pub struct Binding {
pub kind: BindingKind,
pub location: Location,
pub used: Option<usize>,
}
use crate::ast::types::{BindingKind, Scope};
/// Extract the names bound to a given __all__ assignment.
pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
@@ -72,7 +22,7 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
if let StmtKind::AugAssign { .. } = &stmt.node {
if let Some(binding) = scope.values.get("__all__") {
if let BindingKind::Export(existing) = &binding.kind {
names.extend(existing.clone());
names.extend_from_slice(existing);
}
}
}
@@ -116,6 +66,58 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
names
}
/// Check if a node is parent of a conditional branch.
pub fn on_conditional_branch(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
for index in parent_stack.iter().rev() {
let parent = parents[*index];
if matches!(parent.node, StmtKind::If { .. } | StmtKind::While { .. }) {
return true;
}
if let StmtKind::Expr { value } = &parent.node {
if matches!(value.node, ExprKind::IfExp { .. }) {
return true;
}
}
}
false
}
/// Check if a node is in a nested block.
pub fn in_nested_block(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
for index in parent_stack.iter().rev() {
let parent = parents[*index];
if matches!(
parent.node,
StmtKind::Try { .. } | StmtKind::If { .. } | StmtKind::With { .. }
) {
return true;
}
}
false
}
/// Check if a node represents an unpacking assignment.
pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
if let StmtKind::Assign { targets, value, .. } = &stmt.node {
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,
_ => {}
}
return true;
}
false
}
/// Struct used to efficiently slice source code at (row, column) Locations.
pub struct SourceCodeLocator<'a> {
content: &'a str,

137
src/ast/relocate.rs Normal file
View File

@@ -0,0 +1,137 @@
use rustpython_parser::ast::{Expr, ExprKind, Keyword, Location};
fn relocate_keyword(keyword: &mut Keyword, location: Location) {
keyword.location = location;
relocate_expr(&mut keyword.node.value, location);
}
/// Change an expression's location (recursively) to match a desired, fixed location.
pub fn relocate_expr(expr: &mut Expr, location: Location) {
expr.location = location;
match &mut expr.node {
ExprKind::BoolOp { values, .. } => {
for expr in values {
relocate_expr(expr, location);
}
}
ExprKind::NamedExpr { target, value } => {
relocate_expr(target, location);
relocate_expr(value, location);
}
ExprKind::BinOp { left, right, .. } => {
relocate_expr(left, location);
relocate_expr(right, location);
}
ExprKind::UnaryOp { operand, .. } => {
relocate_expr(operand, location);
}
ExprKind::Lambda { body, .. } => {
relocate_expr(body, location);
}
ExprKind::IfExp { test, body, orelse } => {
relocate_expr(test, location);
relocate_expr(body, location);
relocate_expr(orelse, location);
}
ExprKind::Dict { keys, values } => {
for expr in keys {
relocate_expr(expr, location);
}
for expr in values {
relocate_expr(expr, location);
}
}
ExprKind::Set { elts } => {
for expr in elts {
relocate_expr(expr, location);
}
}
ExprKind::ListComp { elt, .. } => {
relocate_expr(elt, location);
}
ExprKind::SetComp { elt, .. } => {
relocate_expr(elt, location);
}
ExprKind::DictComp { key, value, .. } => {
relocate_expr(key, location);
relocate_expr(value, location);
}
ExprKind::GeneratorExp { elt, .. } => {
relocate_expr(elt, location);
}
ExprKind::Await { value } => relocate_expr(value, location),
ExprKind::Yield { value } => {
if let Some(expr) = value {
relocate_expr(expr, location);
}
}
ExprKind::YieldFrom { value } => relocate_expr(value, location),
ExprKind::Compare {
left, comparators, ..
} => {
relocate_expr(left, location);
for expr in comparators {
relocate_expr(expr, location);
}
}
ExprKind::Call {
func,
args,
keywords,
} => {
relocate_expr(func, location);
for expr in args {
relocate_expr(expr, location);
}
for keyword in keywords {
relocate_keyword(keyword, location);
}
}
ExprKind::FormattedValue {
value, format_spec, ..
} => {
relocate_expr(value, location);
if let Some(expr) = format_spec {
relocate_expr(expr, location);
}
}
ExprKind::JoinedStr { values } => {
for expr in values {
relocate_expr(expr, location);
}
}
ExprKind::Constant { .. } => {}
ExprKind::Attribute { value, .. } => {
relocate_expr(value, location);
}
ExprKind::Subscript { value, slice, .. } => {
relocate_expr(value, location);
relocate_expr(slice, location);
}
ExprKind::Starred { value, .. } => {
relocate_expr(value, location);
}
ExprKind::Name { .. } => {}
ExprKind::List { elts, .. } => {
for expr in elts {
relocate_expr(expr, location);
}
}
ExprKind::Tuple { elts, .. } => {
for expr in elts {
relocate_expr(expr, location);
}
}
ExprKind::Slice { lower, upper, step } => {
if let Some(expr) = lower {
relocate_expr(expr, location);
}
if let Some(expr) = upper {
relocate_expr(expr, location);
}
if let Some(expr) = step {
relocate_expr(expr, location);
}
}
}
}

71
src/ast/types.rs Normal file
View File

@@ -0,0 +1,71 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use rustpython_parser::ast::Location;
fn id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(1);
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(FunctionScope),
Generator,
Module,
}
#[derive(Clone, Debug)]
pub struct Scope {
pub id: usize,
pub kind: ScopeKind,
pub import_starred: bool,
pub values: BTreeMap<String, Binding>,
}
impl Scope {
pub fn new(kind: ScopeKind) -> Self {
Scope {
id: id(),
kind,
import_starred: false,
values: BTreeMap::new(),
}
}
}
#[derive(Clone, Debug)]
pub enum BindingKind {
Annotation,
Argument,
Assignment,
Binding,
LoopVar,
Builtin,
ClassDefinition,
Definition,
Export(Vec<String>),
FutureImportation,
Importation(String),
StarImportation,
SubmoduleImportation(String),
}
#[derive(Clone, Debug)]
pub struct Binding {
pub kind: BindingKind,
pub location: Location,
/// Tuple of (scope index, location) indicating the scope and location at which the binding was
/// last used.
pub used: Option<(usize, Location)>,
}
pub trait CheckLocator {
fn locate_check(&self, default: Location) -> Location;
}

View File

@@ -4,102 +4,141 @@ use rustpython_parser::ast::{
PatternKind, Stmt, StmtKind, Unaryop, Withitem,
};
pub trait Visitor {
fn visit_stmt(&mut self, stmt: &Stmt) {
pub trait Visitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
walk_stmt(self, stmt);
}
fn visit_annotation(&mut self, expr: &Expr) {
fn visit_annotation(&mut self, expr: &'a Expr) {
walk_expr(self, expr);
}
fn visit_expr(&mut self, expr: &Expr, _parent: Option<&Stmt>) {
fn visit_expr(&mut self, expr: &'a Expr) {
walk_expr(self, expr);
}
fn visit_constant(&mut self, constant: &Constant) {
fn visit_constant(&mut self, constant: &'a Constant) {
walk_constant(self, constant);
}
fn visit_expr_context(&mut self, expr_content: &ExprContext) {
fn visit_expr_context(&mut self, expr_content: &'a ExprContext) {
walk_expr_context(self, expr_content);
}
fn visit_boolop(&mut self, boolop: &Boolop) {
fn visit_boolop(&mut self, boolop: &'a Boolop) {
walk_boolop(self, boolop);
}
fn visit_operator(&mut self, operator: &Operator) {
fn visit_operator(&mut self, operator: &'a Operator) {
walk_operator(self, operator);
}
fn visit_unaryop(&mut self, unaryop: &Unaryop) {
fn visit_unaryop(&mut self, unaryop: &'a Unaryop) {
walk_unaryop(self, unaryop);
}
fn visit_cmpop(&mut self, cmpop: &Cmpop) {
fn visit_cmpop(&mut self, cmpop: &'a Cmpop) {
walk_cmpop(self, cmpop);
}
fn visit_comprehension(&mut self, comprehension: &Comprehension) {
fn visit_comprehension(&mut self, comprehension: &'a Comprehension) {
walk_comprehension(self, comprehension);
}
fn visit_excepthandler(&mut self, excepthandler: &Excepthandler) {
fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) {
walk_excepthandler(self, excepthandler);
}
fn visit_arguments(&mut self, arguments: &Arguments) {
fn visit_arguments(&mut self, arguments: &'a Arguments) {
walk_arguments(self, arguments);
}
fn visit_arg(&mut self, arg: &Arg) {
fn visit_arg(&mut self, arg: &'a Arg) {
walk_arg(self, arg);
}
fn visit_keyword(&mut self, keyword: &Keyword) {
fn visit_keyword(&mut self, keyword: &'a Keyword) {
walk_keyword(self, keyword);
}
fn visit_alias(&mut self, alias: &Alias) {
fn visit_alias(&mut self, alias: &'a Alias) {
walk_alias(self, alias);
}
fn visit_withitem(&mut self, withitem: &Withitem) {
fn visit_withitem(&mut self, withitem: &'a Withitem) {
walk_withitem(self, withitem);
}
fn visit_match_case(&mut self, match_case: &MatchCase) {
fn visit_match_case(&mut self, match_case: &'a MatchCase) {
walk_match_case(self, match_case);
}
fn visit_pattern(&mut self, pattern: &Pattern) {
fn visit_pattern(&mut self, pattern: &'a Pattern) {
walk_pattern(self, pattern);
}
}
pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) {
match &stmt.node {
StmtKind::FunctionDef { args, body, .. } => {
StmtKind::FunctionDef {
args,
body,
decorator_list,
returns,
..
} => {
visitor.visit_arguments(args);
for expr in decorator_list {
visitor.visit_expr(expr);
}
for expr in returns {
visitor.visit_annotation(expr);
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::AsyncFunctionDef { args, body, .. } => {
StmtKind::AsyncFunctionDef {
args,
body,
decorator_list,
returns,
..
} => {
visitor.visit_arguments(args);
for expr in decorator_list {
visitor.visit_expr(expr);
}
for expr in returns {
visitor.visit_annotation(expr);
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::ClassDef { body, .. } => {
StmtKind::ClassDef {
bases,
keywords,
body,
decorator_list,
..
} => {
for expr in bases {
visitor.visit_expr(expr);
}
for keyword in keywords {
visitor.visit_keyword(keyword);
}
for expr in decorator_list {
visitor.visit_expr(expr);
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::Return { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
}
}
StmtKind::Delete { targets } => {
for expr in targets {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
}
}
StmtKind::Assign { targets, value, .. } => {
visitor.visit_expr(value);
for expr in targets {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
}
visitor.visit_expr(value, Some(stmt))
}
StmtKind::AugAssign { target, op, value } => {
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(target);
visitor.visit_operator(op);
visitor.visit_expr(value, Some(stmt));
visitor.visit_expr(value);
}
StmtKind::AnnAssign {
target,
@@ -107,11 +146,11 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
value,
..
} => {
visitor.visit_expr(target, Some(stmt));
visitor.visit_annotation(annotation);
if let Some(expr) = value {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
}
visitor.visit_expr(target);
}
StmtKind::For {
target,
@@ -120,13 +159,13 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
orelse,
..
} => {
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(iter, Some(stmt));
visitor.visit_expr(target);
visitor.visit_expr(iter);
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for stmt in orelse {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::AsyncFor {
@@ -136,31 +175,31 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
orelse,
..
} => {
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(iter, Some(stmt));
visitor.visit_expr(target);
visitor.visit_expr(iter);
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for stmt in orelse {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::While { test, body, orelse } => {
visitor.visit_expr(test, Some(stmt));
visitor.visit_expr(test);
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for stmt in orelse {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::If { test, body, orelse } => {
visitor.visit_expr(test, Some(stmt));
visitor.visit_expr(test);
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for stmt in orelse {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::With { items, body, .. } => {
@@ -168,7 +207,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
visitor.visit_withitem(withitem);
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::AsyncWith { items, body, .. } => {
@@ -176,22 +215,22 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
visitor.visit_withitem(withitem);
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::Match { subject, cases } => {
// TODO(charlie): Handle `cases`.
visitor.visit_expr(subject, Some(stmt));
visitor.visit_expr(subject);
for match_case in cases {
visitor.visit_match_case(match_case);
}
}
StmtKind::Raise { exc, cause } => {
if let Some(expr) = exc {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
};
if let Some(expr) = cause {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
};
}
StmtKind::Try {
@@ -201,22 +240,22 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
finalbody,
} => {
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for excepthandler in handlers {
visitor.visit_excepthandler(excepthandler)
}
for stmt in orelse {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
for stmt in finalbody {
visitor.visit_stmt(stmt)
visitor.visit_stmt(stmt);
}
}
StmtKind::Assert { test, msg } => {
visitor.visit_expr(test, None);
visitor.visit_expr(test);
if let Some(expr) = msg {
visitor.visit_expr(expr, Some(stmt))
visitor.visit_expr(expr);
}
}
StmtKind::Import { names } => {
@@ -231,67 +270,67 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
StmtKind::Global { .. } => {}
StmtKind::Nonlocal { .. } => {}
StmtKind::Expr { value } => visitor.visit_expr(value, Some(stmt)),
StmtKind::Expr { value } => visitor.visit_expr(value),
StmtKind::Pass => {}
StmtKind::Break => {}
StmtKind::Continue => {}
}
}
pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
match &expr.node {
ExprKind::BoolOp { op, values } => {
visitor.visit_boolop(op);
for expr in values {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::NamedExpr { target, value } => {
visitor.visit_expr(target, None);
visitor.visit_expr(value, None);
visitor.visit_expr(target);
visitor.visit_expr(value);
}
ExprKind::BinOp { left, op, right } => {
visitor.visit_expr(left, None);
visitor.visit_expr(left);
visitor.visit_operator(op);
visitor.visit_expr(right, None);
visitor.visit_expr(right);
}
ExprKind::UnaryOp { op, operand } => {
visitor.visit_unaryop(op);
visitor.visit_expr(operand, None);
visitor.visit_expr(operand);
}
ExprKind::Lambda { args, body } => {
visitor.visit_arguments(args);
visitor.visit_expr(body, None);
visitor.visit_expr(body);
}
ExprKind::IfExp { test, body, orelse } => {
visitor.visit_expr(test, None);
visitor.visit_expr(body, None);
visitor.visit_expr(orelse, None);
visitor.visit_expr(test);
visitor.visit_expr(body);
visitor.visit_expr(orelse);
}
ExprKind::Dict { keys, values } => {
for expr in keys {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
for expr in values {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::Set { elts } => {
for expr in elts {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::ListComp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(elt, None);
visitor.visit_expr(elt);
}
ExprKind::SetComp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(elt, None);
visitor.visit_expr(elt);
}
ExprKind::DictComp {
key,
@@ -299,35 +338,35 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
generators,
} => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(key, None);
visitor.visit_expr(value, None);
visitor.visit_expr(key);
visitor.visit_expr(value);
}
ExprKind::GeneratorExp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(elt, None);
visitor.visit_expr(elt);
}
ExprKind::Await { value } => visitor.visit_expr(value, None),
ExprKind::Await { value } => visitor.visit_expr(value),
ExprKind::Yield { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::YieldFrom { value } => visitor.visit_expr(value, None),
ExprKind::YieldFrom { value } => visitor.visit_expr(value),
ExprKind::Compare {
left,
ops,
comparators,
} => {
visitor.visit_expr(left, None);
visitor.visit_expr(left);
for cmpop in ops {
visitor.visit_cmpop(cmpop);
}
for expr in comparators {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::Call {
@@ -335,9 +374,9 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
args,
keywords,
} => {
visitor.visit_expr(func, None);
visitor.visit_expr(func);
for expr in args {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
for keyword in keywords {
visitor.visit_keyword(keyword);
@@ -346,28 +385,28 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
ExprKind::FormattedValue {
value, format_spec, ..
} => {
visitor.visit_expr(value, None);
visitor.visit_expr(value);
if let Some(expr) = format_spec {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::JoinedStr { values } => {
for expr in values {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
ExprKind::Constant { value, .. } => visitor.visit_constant(value),
ExprKind::Attribute { value, ctx, .. } => {
visitor.visit_expr(value, None);
visitor.visit_expr(value);
visitor.visit_expr_context(ctx);
}
ExprKind::Subscript { value, slice, ctx } => {
visitor.visit_expr(value, None);
visitor.visit_expr(slice, None);
visitor.visit_expr(value);
visitor.visit_expr(slice);
visitor.visit_expr_context(ctx);
}
ExprKind::Starred { value, ctx } => {
visitor.visit_expr(value, None);
visitor.visit_expr(value);
visitor.visit_expr_context(ctx);
}
ExprKind::Name { ctx, .. } => {
@@ -375,31 +414,31 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
}
ExprKind::List { elts, ctx } => {
for expr in elts {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
visitor.visit_expr_context(ctx);
}
ExprKind::Tuple { elts, ctx } => {
for expr in elts {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
visitor.visit_expr_context(ctx);
}
ExprKind::Slice { lower, upper, step } => {
if let Some(expr) = lower {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
if let Some(expr) = upper {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
if let Some(expr) = step {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
}
}
}
pub fn walk_constant<V: Visitor + ?Sized>(visitor: &mut V, constant: &Constant) {
pub fn walk_constant<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, constant: &'a Constant) {
if let Constant::Tuple(constants) = constant {
for constant in constants {
visitor.visit_constant(constant)
@@ -407,19 +446,25 @@ pub fn walk_constant<V: Visitor + ?Sized>(visitor: &mut V, constant: &Constant)
}
}
pub fn walk_comprehension<V: Visitor + ?Sized>(visitor: &mut V, comprehension: &Comprehension) {
visitor.visit_expr(&comprehension.target, None);
visitor.visit_expr(&comprehension.iter, None);
pub fn walk_comprehension<'a, V: Visitor<'a> + ?Sized>(
visitor: &mut V,
comprehension: &'a Comprehension,
) {
visitor.visit_expr(&comprehension.target);
visitor.visit_expr(&comprehension.iter);
for expr in &comprehension.ifs {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
}
pub fn walk_excepthandler<V: Visitor + ?Sized>(visitor: &mut V, excepthandler: &Excepthandler) {
pub fn walk_excepthandler<'a, V: Visitor<'a> + ?Sized>(
visitor: &mut V,
excepthandler: &'a Excepthandler,
) {
match &excepthandler.node {
ExcepthandlerKind::ExceptHandler { type_, body, .. } => {
if let Some(expr) = type_ {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
for stmt in body {
visitor.visit_stmt(stmt);
@@ -428,7 +473,7 @@ pub fn walk_excepthandler<V: Visitor + ?Sized>(visitor: &mut V, excepthandler: &
}
}
pub fn walk_arguments<V: Visitor + ?Sized>(visitor: &mut V, arguments: &Arguments) {
pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) {
for arg in &arguments.posonlyargs {
visitor.visit_arg(arg);
}
@@ -436,61 +481,61 @@ pub fn walk_arguments<V: Visitor + ?Sized>(visitor: &mut V, arguments: &Argument
visitor.visit_arg(arg);
}
if let Some(arg) = &arguments.vararg {
visitor.visit_arg(arg)
visitor.visit_arg(arg);
}
for arg in &arguments.kwonlyargs {
visitor.visit_arg(arg);
}
for expr in &arguments.kw_defaults {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
if let Some(arg) = &arguments.kwarg {
visitor.visit_arg(arg)
visitor.visit_arg(arg);
}
for expr in &arguments.defaults {
visitor.visit_expr(expr, None)
visitor.visit_expr(expr);
}
}
pub fn walk_arg<V: Visitor + ?Sized>(visitor: &mut V, arg: &Arg) {
pub fn walk_arg<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arg: &'a Arg) {
if let Some(expr) = &arg.node.annotation {
visitor.visit_annotation(expr)
visitor.visit_annotation(expr);
}
}
pub fn walk_keyword<V: Visitor + ?Sized>(visitor: &mut V, keyword: &Keyword) {
visitor.visit_expr(&keyword.node.value, None);
pub fn walk_keyword<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, keyword: &'a Keyword) {
visitor.visit_expr(&keyword.node.value);
}
pub fn walk_withitem<V: Visitor + ?Sized>(visitor: &mut V, withitem: &Withitem) {
visitor.visit_expr(&withitem.context_expr, None);
pub fn walk_withitem<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, withitem: &'a Withitem) {
visitor.visit_expr(&withitem.context_expr);
if let Some(expr) = &withitem.optional_vars {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
}
pub fn walk_match_case<V: Visitor + ?Sized>(visitor: &mut V, match_case: &MatchCase) {
pub fn walk_match_case<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, match_case: &'a MatchCase) {
visitor.visit_pattern(&match_case.pattern);
if let Some(expr) = &match_case.guard {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
for stmt in &match_case.body {
visitor.visit_stmt(stmt);
}
}
pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
pub fn walk_pattern<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, pattern: &'a Pattern) {
match &pattern.node {
PatternKind::MatchValue { value } => visitor.visit_expr(value, None),
PatternKind::MatchValue { value } => visitor.visit_expr(value),
PatternKind::MatchSingleton { value } => visitor.visit_constant(value),
PatternKind::MatchSequence { patterns } => {
for pattern in patterns {
visitor.visit_pattern(pattern)
visitor.visit_pattern(pattern);
}
}
PatternKind::MatchMapping { keys, patterns, .. } => {
for expr in keys {
visitor.visit_expr(expr, None);
visitor.visit_expr(expr);
}
for pattern in patterns {
visitor.visit_pattern(pattern);
@@ -502,7 +547,7 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
kwd_patterns,
..
} => {
visitor.visit_expr(cls, None);
visitor.visit_expr(cls);
for pattern in patterns {
visitor.visit_pattern(pattern);
}
@@ -514,7 +559,7 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
PatternKind::MatchStar { .. } => {}
PatternKind::MatchAs { pattern, .. } => {
if let Some(pattern) = pattern {
visitor.visit_pattern(pattern)
visitor.visit_pattern(pattern);
}
}
PatternKind::MatchOr { patterns } => {
@@ -527,24 +572,28 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_expr_context<V: Visitor + ?Sized>(visitor: &mut V, expr_context: &ExprContext) {}
pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>(
visitor: &mut V,
expr_context: &'a ExprContext,
) {
}
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_boolop<V: Visitor + ?Sized>(visitor: &mut V, boolop: &Boolop) {}
pub fn walk_boolop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, boolop: &'a Boolop) {}
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_operator<V: Visitor + ?Sized>(visitor: &mut V, operator: &Operator) {}
pub fn walk_operator<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, operator: &'a Operator) {}
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_unaryop<V: Visitor + ?Sized>(visitor: &mut V, unaryop: &Unaryop) {}
pub fn walk_unaryop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, unaryop: &'a Unaryop) {}
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_cmpop<V: Visitor + ?Sized>(visitor: &mut V, cmpop: &Cmpop) {}
pub fn walk_cmpop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, cmpop: &'a Cmpop) {}
#[allow(unused_variables)]
#[inline(always)]
pub fn walk_alias<V: Visitor + ?Sized>(visitor: &mut V, alias: &Alias) {}
pub fn walk_alias<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, alias: &'a Alias) {}

View File

@@ -1,216 +1,2 @@
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,
Apply,
None,
}
impl From<bool> for Mode {
fn from(value: bool) -> Self {
match value {
true => Mode::Apply,
false => Mode::None,
}
}
}
/// 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::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(())
}
}
pub mod fixer;
pub mod fixes;

216
src/autofix/fixer.rs Normal file
View File

@@ -0,0 +1,216 @@
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,
Apply,
None,
}
impl From<bool> for Mode {
fn from(value: bool) -> Self {
match value {
true => Mode::Apply,
false => Mode::None,
}
}
}
/// 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,8 +1,8 @@
use crate::ast_ops::SourceCodeLocator;
use rustpython_parser::ast::{Expr, Keyword, Location};
use rustpython_parser::lexer;
use rustpython_parser::token::Tok;
use crate::ast::operations::SourceCodeLocator;
use crate::checks::Fix;
/// Convert a location within a file (relative to `base`) to an absolute position.

View File

@@ -1,14 +1,17 @@
use std::collections::hash_map::DefaultHasher;
use std::fs::Metadata;
use std::fs::{create_dir_all, File, Metadata};
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::path::Path;
use crate::autofix;
use anyhow::Result;
use cacache::Error::EntryNotFound;
use filetime::FileTime;
use log::error;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
use crate::autofix::fixer;
use crate::message::Message;
use crate::settings::Settings;
@@ -62,7 +65,7 @@ impl From<bool> for Mode {
fn from(value: bool) -> Self {
match value {
true => Mode::ReadWrite,
false => Mode::WriteOnly,
false => Mode::None,
}
}
}
@@ -71,23 +74,33 @@ fn cache_dir() -> &'static str {
"./.ruff_cache"
}
fn cache_key(path: &Path, settings: &Settings, autofix: &autofix::Mode) -> String {
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(),
path.absolutize().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: &autofix::Mode,
autofix: &fixer::Mode,
mode: &Mode,
) -> Option<Vec<Message>> {
if !mode.allow_read() {
@@ -116,7 +129,7 @@ pub fn set(
path: &Path,
metadata: &Metadata,
settings: &Settings,
autofix: &autofix::Mode,
autofix: &fixer::Mode,
messages: &[Message],
mode: &Mode,
) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,16 @@
use std::collections::BTreeMap;
use rustpython_parser::ast::Location;
use crate::checks::{Check, CheckKind};
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::noqa;
use crate::noqa::Directive;
use crate::settings::Settings;
/// Whether the given line is too long and should be reported.
fn should_enforce_line_length(line: &str, limit: usize) -> bool {
if line.len() > limit {
fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
if length > limit {
let mut chunks = line.split_whitespace();
if let (Some(first), Some(_)) = (chunks.next(), chunks.next()) {
// Do not enforce the line length for commented lines with a single word
@@ -19,34 +24,191 @@ fn should_enforce_line_length(line: &str, limit: usize) -> bool {
}
}
pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings) {
let enforce_line_too_long = settings.select.contains(CheckKind::LineTooLong.code());
pub fn check_lines(
checks: &mut Vec<Check>,
contents: &str,
noqa_line_for: &[usize],
settings: &Settings,
autofix: &fixer::Mode,
) {
let enforce_line_too_long = settings.select.contains(&CheckCode::E501);
let enforce_noqa = settings.select.contains(&CheckCode::M001);
let mut noqa_directives: BTreeMap<usize, (Directive, Vec<&str>)> = BTreeMap::new();
let mut line_checks = vec![];
let mut ignored = vec![];
for (row, line) in contents.lines().enumerate() {
let lines: Vec<&str> = contents.lines().collect();
for (lineno, line) in lines.iter().enumerate() {
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let noqa_lineno = noqa_line_for
.get(lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
if enforce_noqa {
noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
}
// Remove any ignored checks.
// TODO(charlie): Only validate checks for the current line.
for (index, check) in checks.iter().enumerate() {
if check.location.row() == row + 1 && check.is_inline_ignored(line) {
ignored.push(index);
if check.location.row() == lineno + 1 {
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
match noqa {
(Directive::All(_), matches) => {
matches.push(check.kind.code().as_str());
ignored.push(index)
}
(Directive::Codes(_, codes), matches) => {
if codes.contains(&check.kind.code().as_str()) {
matches.push(check.kind.code().as_str());
ignored.push(index);
}
}
(Directive::None, _) => {}
}
}
}
// Enforce line length.
if enforce_line_too_long && should_enforce_line_length(line, settings.line_length) {
let check = Check::new(
CheckKind::LineTooLong,
Location::new(row + 1, settings.line_length + 1),
);
if !check.is_inline_ignored(line) {
line_checks.push(check);
if enforce_line_too_long {
let line_length = line.chars().count();
if should_enforce_line_length(line, line_length, settings.line_length) {
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
let check = Check::new(
CheckKind::LineTooLong(line_length, settings.line_length),
Location::new(lineno + 1, settings.line_length + 1),
);
match noqa {
(Directive::All(_), matches) => {
matches.push(check.kind.code().as_str());
}
(Directive::Codes(_, codes), matches) => {
if codes.contains(&check.kind.code().as_str()) {
matches.push(check.kind.code().as_str());
} else {
line_checks.push(check);
}
}
(Directive::None, _) => line_checks.push(check),
}
}
}
}
// Enforce that the noqa directive was actually used.
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
match directive {
Directive::All(column) => {
if matches.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(None),
Location::new(row + 1, column + 1),
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "".to_string(),
start: Location::new(row + 1, column + 1),
end: Location::new(row + 1, lines[row].chars().count() + 1),
applied: false,
});
}
line_checks.push(check);
}
}
Directive::Codes(column, codes) => {
let mut invalid_codes = vec![];
let mut valid_codes = vec![];
for code in codes {
if !matches.contains(&code) {
invalid_codes.push(code);
} else {
valid_codes.push(code);
}
}
if !invalid_codes.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(invalid_codes.join(", "))),
Location::new(row + 1, column + 1),
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if valid_codes.is_empty() {
check.amend(Fix {
content: "".to_string(),
start: Location::new(row + 1, column + 1),
end: Location::new(row + 1, lines[row].chars().count() + 1),
applied: false,
});
} else {
check.amend(Fix {
content: format!(" # noqa: {}", valid_codes.join(", ")),
start: Location::new(row + 1, column + 1),
end: Location::new(row + 1, lines[row].chars().count() + 1),
applied: false,
});
}
}
line_checks.push(check);
}
}
Directive::None => {}
}
}
}
ignored.sort();
for index in ignored.iter().rev() {
checks.swap_remove(*index);
}
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 noqa_line_for: Vec<usize> = vec![1];
let check_with_max_line_length = |line_length: usize| {
let mut checks: Vec<Check> = vec![];
let settings = Settings {
pyproject: None,
project_root: None,
line_length,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from_iter(vec![CheckCode::E501]),
};
check_lines(
&mut checks,
line,
&noqa_line_for,
&settings,
&fixer::Mode::Generate,
);
return checks;
};
assert!(!check_with_max_line_length(6).is_empty());
assert!(check_with_max_line_length(7).is_empty());
}
}

View File

@@ -1,23 +1,140 @@
use std::str::FromStr;
use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord)]
pub const DEFAULT_CHECK_CODES: [CheckCode; 42] = [
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,
];
pub const ALL_CHECK_CODES: [CheckCode; 45] = [
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E721,
CheckCode::E722,
CheckCode::E731,
CheckCode::E741,
CheckCode::E742,
CheckCode::E743,
CheckCode::E902,
CheckCode::E999,
CheckCode::F401,
CheckCode::F402,
CheckCode::F403,
CheckCode::F404,
CheckCode::F405,
CheckCode::F406,
CheckCode::F407,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F632,
CheckCode::F633,
CheckCode::F634,
CheckCode::F701,
CheckCode::F702,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F722,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
CheckCode::M001,
CheckCode::R001,
CheckCode::R002,
];
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord)]
pub enum CheckCode {
E402,
E501,
E711,
E712,
E713,
E714,
E721,
E722,
E731,
E741,
E742,
E743,
E902,
E999,
F401,
F402,
F403,
F404,
F405,
F406,
F407,
F541,
F601,
F602,
F621,
F622,
F631,
F632,
F633,
F634,
F701,
F702,
F704,
F706,
F707,
F722,
F821,
F822,
F823,
@@ -26,6 +143,7 @@ pub enum CheckCode {
F901,
R001,
R002,
M001,
}
impl FromStr for CheckCode {
@@ -35,14 +153,40 @@ impl FromStr for CheckCode {
match s {
"E402" => Ok(CheckCode::E402),
"E501" => Ok(CheckCode::E501),
"E711" => Ok(CheckCode::E711),
"E712" => Ok(CheckCode::E712),
"E713" => Ok(CheckCode::E713),
"E714" => Ok(CheckCode::E714),
"E721" => Ok(CheckCode::E721),
"E722" => Ok(CheckCode::E722),
"E731" => Ok(CheckCode::E731),
"E741" => Ok(CheckCode::E741),
"E742" => Ok(CheckCode::E742),
"E743" => Ok(CheckCode::E743),
"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),
"F601" => Ok(CheckCode::F601),
"F602" => Ok(CheckCode::F602),
"F621" => Ok(CheckCode::F621),
"F622" => Ok(CheckCode::F622),
"F631" => Ok(CheckCode::F631),
"F632" => Ok(CheckCode::F632),
"F633" => Ok(CheckCode::F633),
"F634" => Ok(CheckCode::F634),
"F701" => Ok(CheckCode::F701),
"F702" => Ok(CheckCode::F702),
"F704" => Ok(CheckCode::F704),
"F706" => Ok(CheckCode::F706),
"F707" => Ok(CheckCode::F707),
"F722" => Ok(CheckCode::F722),
"F821" => Ok(CheckCode::F821),
"F822" => Ok(CheckCode::F822),
"F823" => Ok(CheckCode::F823),
@@ -51,6 +195,7 @@ impl FromStr for CheckCode {
"F901" => Ok(CheckCode::F901),
"R001" => Ok(CheckCode::R001),
"R002" => Ok(CheckCode::R002),
"M001" => Ok(CheckCode::M001),
_ => Err(anyhow::anyhow!("Unknown check code: {s}")),
}
}
@@ -61,14 +206,40 @@ impl CheckCode {
match self {
CheckCode::E402 => "E402",
CheckCode::E501 => "E501",
CheckCode::E711 => "E711",
CheckCode::E712 => "E712",
CheckCode::E713 => "E713",
CheckCode::E714 => "E714",
CheckCode::E721 => "E721",
CheckCode::E722 => "E722",
CheckCode::E731 => "E731",
CheckCode::E741 => "E741",
CheckCode::E742 => "E742",
CheckCode::E743 => "E743",
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",
CheckCode::F601 => "F601",
CheckCode::F602 => "F602",
CheckCode::F621 => "F621",
CheckCode::F622 => "F622",
CheckCode::F631 => "F631",
CheckCode::F632 => "F632",
CheckCode::F633 => "F633",
CheckCode::F634 => "F634",
CheckCode::F701 => "F701",
CheckCode::F702 => "F702",
CheckCode::F704 => "F704",
CheckCode::F706 => "F706",
CheckCode::F707 => "F707",
CheckCode::F722 => "F722",
CheckCode::F821 => "F821",
CheckCode::F822 => "F822",
CheckCode::F823 => "F823",
@@ -77,30 +248,67 @@ impl CheckCode {
CheckCode::F901 => "F901",
CheckCode::R001 => "R001",
CheckCode::R002 => "R002",
CheckCode::M001 => "M001",
}
}
/// The source for the check (either the AST, or the physical lines).
/// The source for the check (either the AST, the filesystem, or the physical lines).
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E402 => &LintSource::AST,
CheckCode::E501 => &LintSource::Lines,
CheckCode::F401 => &LintSource::AST,
CheckCode::F403 => &LintSource::AST,
CheckCode::F541 => &LintSource::AST,
CheckCode::F631 => &LintSource::AST,
CheckCode::F634 => &LintSource::AST,
CheckCode::F704 => &LintSource::AST,
CheckCode::F706 => &LintSource::AST,
CheckCode::F707 => &LintSource::AST,
CheckCode::F821 => &LintSource::AST,
CheckCode::F822 => &LintSource::AST,
CheckCode::F823 => &LintSource::AST,
CheckCode::F831 => &LintSource::AST,
CheckCode::F841 => &LintSource::AST,
CheckCode::F901 => &LintSource::AST,
CheckCode::R001 => &LintSource::AST,
CheckCode::R002 => &LintSource::AST,
CheckCode::E501 | CheckCode::M001 => &LintSource::Lines,
CheckCode::E902 | CheckCode::E999 => &LintSource::FileSystem,
_ => &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::M001 => CheckKind::UnusedNOQA(None),
CheckCode::R001 => CheckKind::UselessObjectInheritance("...".to_string()),
CheckCode::R002 => CheckKind::NoAssertEquals,
}
}
}
@@ -109,21 +317,55 @@ impl CheckCode {
pub enum LintSource {
AST,
Lines,
FileSystem,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum RejectedCmpop {
Eq,
NotEq,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CheckKind {
UnusedNOQA(Option<String>),
AmbiguousClassName(String),
AmbiguousFunctionName(String),
AmbiguousVariableName(String),
AssertTuple,
BreakOutsideLoop,
ContinueOutsideLoop,
DefaultExceptNotLast,
DoNotAssignLambda,
DoNotUseBareExcept,
DuplicateArgumentName,
ForwardAnnotationSyntaxError(String),
FStringMissingPlaceholders,
FutureFeatureNotDefined(String),
IOError(String),
IfTuple,
ImportStarUsage,
LineTooLong,
ImportShadowedByLoopVar(String, usize),
ImportStarNotPermitted(String),
ImportStarUsage(String, String),
ImportStarUsed(String),
InvalidPrintSyntax,
IsLiteral,
LateFutureImport,
LineTooLong(usize, usize),
ModuleImportNotAtTopOfFile,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable(String),
NoAssertEquals,
NoneComparison(RejectedCmpop),
NotInTest,
NotIsTest,
RaiseNotImplemented,
ReturnOutsideFunction,
SyntaxError(String),
TooManyExpressionsInStarredAssignment,
TrueFalseComparison(bool, RejectedCmpop),
TwoStarredExpressions,
TypeComparison,
UndefinedExport(String),
UndefinedLocal(String),
UndefinedName(String),
@@ -137,17 +379,45 @@ impl CheckKind {
/// The name of the check.
pub fn name(&self) -> &'static str {
match self {
CheckKind::AmbiguousClassName(_) => "AmbiguousClassName",
CheckKind::AmbiguousFunctionName(_) => "AmbiguousFunctionName",
CheckKind::AmbiguousVariableName(_) => "AmbiguousVariableName",
CheckKind::AssertTuple => "AssertTuple",
CheckKind::BreakOutsideLoop => "BreakOutsideLoop",
CheckKind::ContinueOutsideLoop => "ContinueOutsideLoop",
CheckKind::DefaultExceptNotLast => "DefaultExceptNotLast",
CheckKind::DoNotAssignLambda => "DoNotAssignLambda",
CheckKind::DoNotUseBareExcept => "DoNotUseBareExcept",
CheckKind::DuplicateArgumentName => "DuplicateArgumentName",
CheckKind::FStringMissingPlaceholders => "FStringMissingPlaceholders",
CheckKind::ForwardAnnotationSyntaxError(_) => "ForwardAnnotationSyntaxError",
CheckKind::FutureFeatureNotDefined(_) => "FutureFeatureNotDefined",
CheckKind::IOError(_) => "IOError",
CheckKind::IfTuple => "IfTuple",
CheckKind::ImportStarUsage => "ImportStarUsage",
CheckKind::LineTooLong => "LineTooLong",
CheckKind::ImportShadowedByLoopVar(_, _) => "ImportShadowedByLoopVar",
CheckKind::ImportStarNotPermitted(_) => "ImportStarNotPermitted",
CheckKind::ImportStarUsage(_, _) => "ImportStarUsage",
CheckKind::ImportStarUsed(_) => "ImportStarUsed",
CheckKind::InvalidPrintSyntax => "InvalidPrintSyntax",
CheckKind::IsLiteral => "IsLiteral",
CheckKind::LateFutureImport => "LateFutureImport",
CheckKind::LineTooLong(_, _) => "LineTooLong",
CheckKind::ModuleImportNotAtTopOfFile => "ModuleImportNotAtTopOfFile",
CheckKind::MultiValueRepeatedKeyLiteral => "MultiValueRepeatedKeyLiteral",
CheckKind::MultiValueRepeatedKeyVariable(_) => "MultiValueRepeatedKeyVariable",
CheckKind::NoAssertEquals => "NoAssertEquals",
CheckKind::NoneComparison(_) => "NoneComparison",
CheckKind::NotInTest => "NotInTest",
CheckKind::NotIsTest => "NotIsTest",
CheckKind::RaiseNotImplemented => "RaiseNotImplemented",
CheckKind::ReturnOutsideFunction => "ReturnOutsideFunction",
CheckKind::SyntaxError(_) => "SyntaxError",
CheckKind::TooManyExpressionsInStarredAssignment => {
"TooManyExpressionsInStarredAssignment"
}
CheckKind::TrueFalseComparison(_, _) => "TrueFalseComparison",
CheckKind::TwoStarredExpressions => "TwoStarredExpressions",
CheckKind::TypeComparison => "TypeComparison",
CheckKind::UndefinedExport(_) => "UndefinedExport",
CheckKind::UndefinedLocal(_) => "UndefinedLocal",
CheckKind::UndefinedName(_) => "UndefinedName",
@@ -155,27 +425,55 @@ impl CheckKind {
CheckKind::UnusedVariable(_) => "UnusedVariable",
CheckKind::UselessObjectInheritance(_) => "UselessObjectInheritance",
CheckKind::YieldOutsideFunction => "YieldOutsideFunction",
CheckKind::UnusedNOQA(_) => "UnusedNOQA",
}
}
/// A four-letter shorthand code for the check.
pub fn code(&self) -> &'static CheckCode {
match self {
CheckKind::AmbiguousClassName(_) => &CheckCode::E742,
CheckKind::AmbiguousFunctionName(_) => &CheckCode::E743,
CheckKind::AmbiguousVariableName(_) => &CheckCode::E741,
CheckKind::AssertTuple => &CheckCode::F631,
CheckKind::BreakOutsideLoop => &CheckCode::F701,
CheckKind::ContinueOutsideLoop => &CheckCode::F702,
CheckKind::DefaultExceptNotLast => &CheckCode::F707,
CheckKind::DoNotAssignLambda => &CheckCode::E731,
CheckKind::DoNotUseBareExcept => &CheckCode::E722,
CheckKind::DuplicateArgumentName => &CheckCode::F831,
CheckKind::FStringMissingPlaceholders => &CheckCode::F541,
CheckKind::ForwardAnnotationSyntaxError(_) => &CheckCode::F722,
CheckKind::FutureFeatureNotDefined(_) => &CheckCode::F407,
CheckKind::IOError(_) => &CheckCode::E902,
CheckKind::IfTuple => &CheckCode::F634,
CheckKind::ImportStarUsage => &CheckCode::F403,
CheckKind::LineTooLong => &CheckCode::E501,
CheckKind::ImportShadowedByLoopVar(_, _) => &CheckCode::F402,
CheckKind::ImportStarNotPermitted(_) => &CheckCode::F406,
CheckKind::ImportStarUsage(_, _) => &CheckCode::F405,
CheckKind::ImportStarUsed(_) => &CheckCode::F403,
CheckKind::InvalidPrintSyntax => &CheckCode::F633,
CheckKind::IsLiteral => &CheckCode::F632,
CheckKind::LateFutureImport => &CheckCode::F404,
CheckKind::LineTooLong(_, _) => &CheckCode::E501,
CheckKind::ModuleImportNotAtTopOfFile => &CheckCode::E402,
CheckKind::MultiValueRepeatedKeyLiteral => &CheckCode::F601,
CheckKind::MultiValueRepeatedKeyVariable(_) => &CheckCode::F602,
CheckKind::NoAssertEquals => &CheckCode::R002,
CheckKind::NoneComparison(_) => &CheckCode::E711,
CheckKind::NotInTest => &CheckCode::E713,
CheckKind::NotIsTest => &CheckCode::E714,
CheckKind::RaiseNotImplemented => &CheckCode::F901,
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
CheckKind::SyntaxError(_) => &CheckCode::E999,
CheckKind::TooManyExpressionsInStarredAssignment => &CheckCode::F621,
CheckKind::TrueFalseComparison(_, _) => &CheckCode::E712,
CheckKind::TwoStarredExpressions => &CheckCode::F622,
CheckKind::TypeComparison => &CheckCode::E721,
CheckKind::UndefinedExport(_) => &CheckCode::F822,
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
CheckKind::UndefinedName(_) => &CheckCode::F821,
CheckKind::UnusedImport(_) => &CheckCode::F401,
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::UselessObjectInheritance(_) => &CheckCode::R001,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
@@ -185,42 +483,120 @@ impl CheckKind {
/// The body text for the check.
pub fn body(&self) -> String {
match self {
CheckKind::AmbiguousClassName(name) => {
format!("ambiguous class name '{}'", name)
}
CheckKind::AmbiguousFunctionName(name) => {
format!("ambiguous function name '{}'", name)
}
CheckKind::AmbiguousVariableName(name) => {
format!("ambiguous variable name '{}'", name)
}
CheckKind::AssertTuple => {
"Assert test is a non-empty tuple, which is always `True`".to_string()
}
CheckKind::BreakOutsideLoop => "`break` outside loop".to_string(),
CheckKind::ContinueOutsideLoop => "`continue` not properly in loop".to_string(),
CheckKind::DefaultExceptNotLast => {
"an `except:` block as not the last exception handler".to_string()
}
CheckKind::DoNotAssignLambda => {
"Do not assign a lambda expression, use a def".to_string()
}
CheckKind::DoNotUseBareExcept => "Do not use bare `except`".to_string(),
CheckKind::DuplicateArgumentName => {
"Duplicate argument name in function definition".to_string()
}
CheckKind::ForwardAnnotationSyntaxError(body) => {
format!("syntax error in forward annotation '{body}'")
}
CheckKind::FStringMissingPlaceholders => {
"f-string without any placeholders".to_string()
}
CheckKind::FutureFeatureNotDefined(name) => {
format!("future feature '{name}' is not defined")
}
CheckKind::IOError(message) => message.clone(),
CheckKind::IfTuple => "If test is a tuple, which is always `True`".to_string(),
CheckKind::ImportStarUsage => "Unable to detect undefined names".to_string(),
CheckKind::LineTooLong => "Line too long".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::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()
}
CheckKind::LineTooLong(length, limit) => {
format!("Line too long ({length} > {limit} characters)")
}
CheckKind::ModuleImportNotAtTopOfFile => {
"Module level import not at top of file".to_string()
}
CheckKind::MultiValueRepeatedKeyLiteral => {
"Dictionary key literal repeated".to_string()
}
CheckKind::MultiValueRepeatedKeyVariable(name) => {
format!("Dictionary key `{name}` repeated")
}
CheckKind::NoAssertEquals => {
"`assertEquals` is deprecated, use `assertEqual` instead".to_string()
}
CheckKind::NoneComparison(op) => match op {
RejectedCmpop::Eq => "Comparison to `None` should be `cond is None`".to_string(),
RejectedCmpop::NotEq => {
"Comparison to `None` should be `cond is not None`".to_string()
}
},
CheckKind::NotInTest => "Test for membership should be `not in`".to_string(),
CheckKind::NotIsTest => "Test for object identity should be `is not`".to_string(),
CheckKind::RaiseNotImplemented => {
"`raise NotImplemented` should be `raise NotImplementedError`".to_string()
}
CheckKind::ReturnOutsideFunction => {
"a `return` statement outside of a function/method".to_string()
}
CheckKind::SyntaxError(message) => format!("SyntaxError: {message}"),
CheckKind::TooManyExpressionsInStarredAssignment => {
"too many expressions in star-unpacking assignment".to_string()
}
CheckKind::TrueFalseComparison(value, op) => match *value {
true => match op {
RejectedCmpop::Eq => {
"Comparison to `True` should be `cond is True`".to_string()
}
RejectedCmpop::NotEq => {
"Comparison to `True` should be `cond is not True`".to_string()
}
},
false => match op {
RejectedCmpop::Eq => {
"Comparison to `False` should be `cond is False`".to_string()
}
RejectedCmpop::NotEq => {
"Comparison to `False` should be `cond is not False`".to_string()
}
},
},
CheckKind::TwoStarredExpressions => "two starred expressions in assignment".to_string(),
CheckKind::TypeComparison => "do not compare types, use `isinstance()`".to_string(),
CheckKind::UndefinedExport(name) => {
format!("Undefined name `{name}` in `__all__`")
}
CheckKind::UndefinedName(name) => {
format!("Undefined name `{name}`")
}
CheckKind::UndefinedLocal(name) => {
format!("Local variable `{name}` referenced before assignment")
}
CheckKind::UndefinedName(name) => {
format!("Undefined name `{name}`")
}
CheckKind::UnusedImport(name) => format!("`{name}` imported but unused"),
CheckKind::UnusedVariable(name) => {
format!("Local variable `{name}` is assigned to but never used")
@@ -231,31 +607,21 @@ impl CheckKind {
CheckKind::YieldOutsideFunction => {
"a `yield` or `yield from` statement outside of a function/method".to_string()
}
CheckKind::UnusedNOQA(code) => match code {
None => "Unused `noqa` directive".to_string(),
Some(code) => format!("Unused `noqa` directive for: {code}"),
},
}
}
/// Whether the check kind is (potentially) fixable.
pub fn fixable(&self) -> bool {
match self {
CheckKind::AssertTuple => false,
CheckKind::DefaultExceptNotLast => false,
CheckKind::DuplicateArgumentName => false,
CheckKind::FStringMissingPlaceholders => false,
CheckKind::IfTuple => false,
CheckKind::ImportStarUsage => false,
CheckKind::LineTooLong => false,
CheckKind::ModuleImportNotAtTopOfFile => false,
CheckKind::NoAssertEquals => true,
CheckKind::RaiseNotImplemented => false,
CheckKind::ReturnOutsideFunction => false,
CheckKind::UndefinedExport(_) => false,
CheckKind::UndefinedLocal(_) => false,
CheckKind::UndefinedName(_) => false,
CheckKind::UnusedImport(_) => false,
CheckKind::UnusedVariable(_) => false,
CheckKind::UselessObjectInheritance(_) => true,
CheckKind::YieldOutsideFunction => false,
}
matches!(
self,
CheckKind::NoAssertEquals
| CheckKind::UselessObjectInheritance(_)
| CheckKind::UnusedNOQA(_)
)
}
}
@@ -274,11 +640,6 @@ pub struct Check {
pub fix: Option<Fix>,
}
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?").expect("Invalid regex")
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").expect("Invalid regex"));
impl Check {
pub fn new(kind: CheckKind, location: Location) -> Self {
Self {
@@ -291,25 +652,4 @@ impl Check {
pub fn amend(&mut self, fix: Fix) {
self.fix = Some(fix);
}
pub fn is_inline_ignored(&self, line: &str) -> bool {
match NO_QA_REGEX.captures(line) {
Some(caps) => match caps.name("codes") {
Some(codes) => {
for code in SPLIT_COMMA_REGEX
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())
{
if code == self.kind.code().as_str() {
return true;
}
}
false
}
None => true,
},
None => false,
}
}
}

240
src/fs.rs
View File

@@ -1,39 +1,142 @@
use std::borrow::Cow;
use std::fs::File;
use std::io::{BufReader, Read};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use anyhow::Result;
use glob::Pattern;
use anyhow::{anyhow, Result};
use log::debug;
use path_absolutize::path_dedot;
use path_absolutize::Absolutize;
use walkdir::{DirEntry, WalkDir};
fn is_not_hidden(entry: &DirEntry) -> bool {
entry
.file_name()
use crate::settings::FilePattern;
/// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
let file_path = path
.to_str()
.map(|s| (entry.depth() == 0 || !s.starts_with('.')))
.unwrap_or(false)
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
let file_basename = path
.file_name()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?
.to_str()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
Ok((file_path, file_basename))
}
fn is_not_excluded(entry: &DirEntry, exclude: &[Pattern]) -> bool {
entry
.path()
.to_str()
.map(|s| !exclude.iter().any(|pattern| pattern.matches(s)))
.unwrap_or(false)
fn is_excluded(file_path: &str, file_basename: &str, exclude: &[FilePattern]) -> bool {
for pattern in exclude {
match &pattern {
FilePattern::Simple(basename) => {
if *basename == file_basename {
return true;
}
}
FilePattern::Complex(absolute, basename) => {
if absolute.matches(file_path) {
return true;
}
if basename
.as_ref()
.map(|pattern| pattern.matches(file_basename))
.unwrap_or_default()
{
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],
) -> impl Iterator<Item = DirEntry> + 'a {
WalkDir::new(path)
path: &'a Path,
exclude: &'a [FilePattern],
extend_exclude: &'a [FilePattern],
) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> + 'a {
// Run some checks over the provided patterns, to enable optimizations below.
let has_exclude = !exclude.is_empty();
let has_extend_exclude = !extend_exclude.is_empty();
let exclude_simple = exclude
.iter()
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
let extend_exclude_simple = extend_exclude
.iter()
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
WalkDir::new(normalize_path(path))
.follow_links(true)
.into_iter()
.filter_entry(|entry| is_not_hidden(entry) && is_not_excluded(entry, exclude))
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().to_string_lossy().ends_with(".py"))
.filter_entry(move |entry| {
if !has_exclude && !has_extend_exclude {
return true;
}
let path = entry.path();
match extract_path_names(path) {
Ok((file_path, file_basename)) => {
let file_type = entry.file_type();
if has_exclude
&& (!exclude_simple || file_type.is_dir())
&& is_excluded(file_path, file_basename, exclude)
{
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if has_extend_exclude
&& (!extend_exclude_simple || file_type.is_dir())
&& is_excluded(file_path, file_basename, extend_exclude)
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
} else {
true
}
}
Err(_) => {
debug!("Ignored path due to error in parsing: {:?}", path);
true
}
}
})
.filter(|entry| {
entry.as_ref().map_or(true, |entry| {
(entry.depth() == 0 && !entry.file_type().is_dir()) || is_included(entry.path())
})
})
}
/// Convert any path to an absolute path (based on the current working directory).
pub fn normalize_path(path: &Path) -> PathBuf {
if let Ok(path) = path.absolutize() {
return path.to_path_buf();
}
path.to_path_buf()
}
/// Convert any path to an absolute path (based on the specified project root).
pub fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
if let Ok(path) = path.absolutize_from(project_root) {
return path.to_path_buf();
}
path.to_path_buf()
}
/// Convert an absolute path to be relative to the current working directory.
pub fn relativize_path(path: &Path) -> Cow<str> {
if let Ok(path) = path.strip_prefix(path_dedot::CWD.deref()) {
return path.to_string_lossy();
}
path.to_string_lossy()
}
/// Read a file's contents from disk.
pub fn read_file(path: &Path) -> Result<String> {
let file = File::open(path)?;
let mut buf_reader = BufReader::new(file);
@@ -41,3 +144,100 @@ pub fn read_file(path: &Path) -> Result<String> {
buf_reader.read_to_string(&mut contents)?;
Ok(contents)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use path_absolutize::Absolutize;
use crate::fs::{extract_path_names, is_excluded, is_included};
use crate::settings::FilePattern;
#[test]
fn inclusions() {
let path = Path::new("foo/bar/baz.py").absolutize().unwrap();
assert!(is_included(&path));
let path = Path::new("foo/bar/baz.pyi").absolutize().unwrap();
assert!(is_included(&path));
let path = Path::new("foo/bar/baz.js").absolutize().unwrap();
assert!(!is_included(&path));
let path = Path::new("foo/bar/baz").absolutize().unwrap();
assert!(!is_included(&path));
}
#[test]
fn exclusions() -> Result<()> {
let project_root = Path::new("/tmp/");
let path = Path::new("foo").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"bar",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz.py",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/baz.py",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/*.py",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(!is_excluded(file_path, file_basename, &exclude));
Ok(())
}
}

View File

@@ -1,15 +1,17 @@
mod ast_ops;
extern crate core;
mod ast;
mod autofix;
mod builtins;
mod cache;
pub mod cache;
pub mod check_ast;
mod check_lines;
pub mod checks;
mod fixer;
pub mod fs;
pub mod linter;
pub mod logging;
pub mod message;
mod pyproject;
mod noqa;
pub mod printer;
pub mod pyproject;
mod python;
pub mod settings;
mod visitor;

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,6 @@
extern crate core;
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
@@ -11,13 +14,18 @@ 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::settings::Settings;
use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::pyproject;
use ::ruff::settings::{FilePattern, Settings};
use ::ruff::tell_user;
use ruff::linter::add_noqa_to_path;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -25,6 +33,7 @@ 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)]
#[clap(version)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::AnyPath, required = true)]
files: Vec<PathBuf>,
@@ -46,165 +55,39 @@ struct Cli {
/// Disable cache reads.
#[clap(short, long, action)]
no_cache: bool,
/// Comma-separated list of error codes to enable.
/// List of error codes to enable.
#[clap(long, multiple = true)]
select: Vec<CheckCode>,
/// Comma-separated list of error codes to ignore.
/// Like --select, but adds additional error codes on top of the selected ones.
#[clap(long, multiple = true)]
extend_select: Vec<CheckCode>,
/// List of error codes to ignore.
#[clap(long, multiple = true)]
ignore: Vec<CheckCode>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[clap(long, multiple = true)]
extend_ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[clap(long, multiple = true)]
exclude: Vec<String>,
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[clap(long, multiple = true)]
extend_exclude: Vec<String>,
/// Output serialization format for error messages.
#[clap(long, arg_enum, default_value_t=SerializationFormat::Text)]
format: SerializationFormat,
/// See the files ruff will be run against with the current settings.
#[clap(long, action)]
show_files: bool,
/// See ruff's settings.
#[clap(long, action)]
show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
#[clap(long, action)]
add_noqa: bool,
}
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 files: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude))
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let mut messages: Vec<Message> = files
.par_iter()
.map(|entry| {
lint_path(entry.path(), settings, &cache.into(), &autofix.into()).unwrap_or_else(|e| {
error!("Failed to check {}: {e:?}", entry.path().to_string_lossy());
vec![]
})
})
.flatten()
.collect();
messages.sort_unstable();
let duration = start.elapsed();
debug!("Checked files in: {:?}", duration);
Ok(messages)
}
fn report_once(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();
if !outstanding.is_empty() {
for message in &outstanding {
println!("{}", message);
}
println!();
}
if !fixed.is_empty() {
println!(
"Found {} error(s) ({} fixed).",
outstanding.len(),
fixed.len()
);
} else {
println!("Found {} error(s).", outstanding.len());
}
if num_fixable > 0 {
println!("{num_fixable} potentially fixable with the --fix option.");
}
Ok(())
}
fn report_continuously(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(())
}
fn inner_main() -> Result<ExitCode> {
let cli = Cli::parse();
set_up_logging(cli.verbose)?;
// TODO(charlie): Can we avoid this cast?
let paths: Vec<&Path> = cli.files.iter().map(PathBuf::as_path).collect();
let mut settings = Settings::from_paths(paths)?;
if !cli.select.is_empty() {
settings.select(cli.select);
}
if !cli.ignore.is_empty() {
settings.ignore(&cli.ignore);
}
if cli.watch {
if cli.fix {
println!("Warning: --fix is not enabled in watch mode.")
}
// Perform an initial run instantly.
clearscreen::clear()?;
tell_user!("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
report_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") {
clearscreen::clear()?;
tell_user!("File change detected...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
report_continuously(&messages)?;
}
}
}
}
Err(e) => return Err(e.into()),
}
}
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
if !cli.quiet {
report_once(&messages)?;
}
check_for_updates();
if !messages.is_empty() && !cli.exit_zero {
return Ok(ExitCode::FAILURE);
}
}
Ok(ExitCode::SUCCESS)
}
#[cfg(feature = "update-informer")]
fn check_for_updates() {
use update_informer::{registry, Check};
@@ -228,6 +111,246 @@ fn check_for_updates() {
}
}
fn show_settings(settings: &Settings) {
println!("{:#?}", settings);
}
fn show_files(files: &[PathBuf], settings: &Settings) {
let mut entries: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flatten()
.collect();
entries.sort_by(|a, b| a.path().cmp(b.path()));
for entry in entries {
println!("{}", entry.path().to_string_lossy());
}
}
fn run_once(
files: &[PathBuf],
settings: &Settings,
cache: bool,
autofix: bool,
) -> Result<Vec<Message>> {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<Result<DirEntry, walkdir::Error>> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let mut messages: Vec<Message> = paths
.par_iter()
.map(|entry| {
match entry {
Ok(entry) => {
let path = entry.path();
lint_path(path, settings, &cache.into(), &autofix.into())
.map_err(|e| (Some(path.to_owned()), e.to_string()))
}
Err(e) => Err((
e.path().map(Path::to_owned),
e.io_error()
.map_or_else(|| e.to_string(), io::Error::to_string),
)),
}
.unwrap_or_else(|(path, message)| {
if let Some(path) = path {
if settings.select.contains(&CheckCode::E902) {
vec![Message {
kind: CheckKind::IOError(message),
fixed: false,
location: Default::default(),
filename: path.to_string_lossy().to_string(),
}]
} else {
error!("Failed to check {}: {message}", path.to_string_lossy());
vec![]
}
} else {
error!("{message}");
vec![]
}
})
})
.flatten()
.collect();
messages.sort_unstable();
let duration = start.elapsed();
debug!("Checked files in: {:?}", duration);
Ok(messages)
}
fn add_noqa(files: &[PathBuf], settings: &Settings) -> Result<usize> {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<Result<DirEntry, walkdir::Error>> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications: usize = paths
.par_iter()
.map(|entry| match entry {
Ok(entry) => {
let path = entry.path();
add_noqa_to_path(path, settings)
}
Err(_) => Ok(0),
})
.flatten()
.sum();
let duration = start.elapsed();
debug!("Added noqa to files in: {:?}", duration);
Ok(modifications)
}
fn inner_main() -> Result<ExitCode> {
let cli = Cli::parse();
set_up_logging(cli.verbose)?;
// Find the project root and pyproject.toml.
let project_root = pyproject::find_project_root(&cli.files);
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
let pyproject = pyproject::find_pyproject_toml(&project_root);
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
// Parse the settings from the pyproject.toml and command-line arguments.
let exclude: Vec<FilePattern> = cli
.exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
let extend_exclude: Vec<FilePattern> = cli
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
let mut settings = Settings::from_pyproject(pyproject, project_root);
if !exclude.is_empty() {
settings.exclude = exclude;
}
if !extend_exclude.is_empty() {
settings.extend_exclude = extend_exclude;
}
if !cli.select.is_empty() {
settings.clear();
settings.select(cli.select);
}
if !cli.extend_select.is_empty() {
settings.select(cli.extend_select);
}
if !cli.ignore.is_empty() {
settings.ignore(&cli.ignore);
}
if !cli.extend_ignore.is_empty() {
settings.ignore(&cli.extend_ignore);
}
if cli.show_settings && cli.show_files {
println!("Error: specify --show-settings or show-files (not both).");
return Ok(ExitCode::FAILURE);
}
if cli.show_settings {
show_settings(&settings);
return Ok(ExitCode::SUCCESS);
}
if cli.show_files {
show_files(&cli.files, &settings);
return Ok(ExitCode::SUCCESS);
}
cache::init()?;
let mut printer = Printer::new(cli.format, cli.verbose);
if cli.watch {
if cli.fix {
println!("Warning: --fix is not enabled in watch mode.");
}
if cli.add_noqa {
println!("Warning: --no-qa is not enabled in watch mode.");
}
if cli.format != SerializationFormat::Text {
println!("Warning: --format 'text' is used in watch mode.");
}
// 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 if cli.add_noqa {
let modifications = add_noqa(&cli.files, &settings)?;
if modifications > 0 {
println!("Added {modifications} noqa directives.");
}
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
if !cli.quiet {
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,

View File

@@ -1,11 +1,13 @@
use std::cmp::Ordering;
use std::fmt;
use std::path::Path;
use colored::Colorize;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use crate::checks::CheckKind;
use crate::fs::relativize_path;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Message {
@@ -20,7 +22,7 @@ impl Ord for Message {
(&self.filename, self.location.row(), self.location.column()).cmp(&(
&other.filename,
other.location.row(),
self.location.column(),
other.location.column(),
))
}
}
@@ -36,7 +38,8 @@ impl fmt::Display for Message {
write!(
f,
"{}{}{}{}{}{} {} {}",
self.filename.white().bold(),
relativize_path(Path::new(&self.filename)).white().bold(),
// self.filename.white(),
":".cyan(),
self.location.row(),
":".cyan(),

280
src/noqa.rs Normal file
View File

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

80
src/printer.rs Normal file
View File

@@ -0,0 +1,80 @@
use anyhow::Result;
use clap::ValueEnum;
use colored::Colorize;
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,
verbose: bool,
}
impl Printer {
pub fn new(format: SerializationFormat, verbose: bool) -> Self {
Self { format, verbose }
}
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 if !outstanding.is_empty() || self.verbose {
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,37 +1,31 @@
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use anyhow::Result;
use common_path::common_path_all;
use log::debug;
use path_absolutize::Absolutize;
use serde::Deserialize;
use crate::checks::CheckCode;
use crate::fs;
pub fn load_config<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<(PathBuf, Config)> {
match find_project_root(paths) {
Some(project_root) => match find_pyproject_toml(&project_root) {
Some(path) => {
debug!("Found pyproject.toml at: {}", path.to_string_lossy());
match parse_pyproject_toml(&path) {
Ok(pyproject) => {
let config = pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default();
Ok((project_root, config))
}
Err(e) => {
println!("Failed to load pyproject.toml: {:?}", e);
println!("Falling back to default configuration...");
Ok(Default::default())
}
}
pub fn load_config(pyproject: &Option<PathBuf>) -> Config {
match pyproject {
Some(pyproject) => match parse_pyproject_toml(pyproject) {
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 => Ok(Default::default()),
},
None => Ok(Default::default()),
None => {
println!("No pyproject.toml found.");
println!("Falling back to default configuration...");
Default::default()
}
}
}
@@ -39,8 +33,10 @@ pub fn load_config<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<(Pat
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Config {
pub line_length: Option<usize>,
pub exclude: Option<Vec<PathBuf>>,
pub select: Option<BTreeSet<CheckCode>>,
pub exclude: Option<Vec<String>>,
pub extend_exclude: Option<Vec<String>>,
pub select: Option<Vec<CheckCode>>,
pub ignore: Option<Vec<CheckCode>>,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
@@ -58,20 +54,34 @@ fn parse_pyproject_toml(path: &Path) -> Result<PyProject> {
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);
pub fn find_pyproject_toml(path: &Option<PathBuf>) -> Option<PathBuf> {
if let Some(path) = path {
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"))
let mut path = dirs::config_dir()?;
path.push("ruff");
path.push("pyproject.toml");
if path.is_file() {
Some(path)
} else {
None
}
}
fn find_project_root<'a>(sources: impl IntoIterator<Item = &'a Path>) -> Option<PathBuf> {
if let Some(prefix) = common_path_all(sources) {
pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
let absolute_sources: Vec<PathBuf> = sources
.iter()
.flat_map(|source| source.absolutize().map(|path| path.to_path_buf()))
.collect();
if let Some(prefix) = common_path_all(absolute_sources.iter().map(PathBuf::as_path)) {
for directory in prefix.ancestors() {
if directory.join(".git").is_dir() {
return Some(directory.to_path_buf());
@@ -90,8 +100,8 @@ fn find_project_root<'a>(sources: impl IntoIterator<Item = &'a Path>) -> Option<
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::path::Path;
use std::env::current_dir;
use std::path::PathBuf;
use anyhow::Result;
@@ -124,7 +134,9 @@ mod tests {
ruff: Some(Config {
line_length: None,
exclude: None,
extend_exclude: None,
select: None,
ignore: None,
})
})
);
@@ -142,7 +154,9 @@ line-length = 79
ruff: Some(Config {
line_length: Some(79),
exclude: None,
extend_exclude: None,
select: None,
ignore: None,
})
})
);
@@ -159,8 +173,10 @@ exclude = ["foo.py"]
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: Some(vec![Path::new("foo.py").to_path_buf()]),
exclude: Some(vec!["foo.py".to_string()]),
extend_exclude: None,
select: None,
ignore: None,
})
})
);
@@ -178,7 +194,29 @@ select = ["E501"]
ruff: Some(Config {
line_length: None,
exclude: None,
select: Some(BTreeSet::from([CheckCode::E501])),
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]),
})
})
);
@@ -216,12 +254,15 @@ other-attribute = 1
#[test]
fn find_and_parse_pyproject_toml() -> Result<()> {
let project_root = find_project_root([Path::new("resources/test/fixtures/__init__.py")])
.expect("Unable to find project root.");
assert_eq!(project_root, Path::new("resources/test/fixtures"));
let cwd = current_dir()?;
let project_root =
find_project_root(&[PathBuf::from("resources/test/fixtures/__init__.py")])
.expect("Unable to find project root.");
assert_eq!(project_root, cwd.join("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 path =
find_pyproject_toml(&Some(project_root)).expect("Unable to find pyproject.toml.");
assert_eq!(path, cwd.join("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject
@@ -232,30 +273,14 @@ other-attribute = 1
config,
Config {
line_length: Some(88),
exclude: Some(vec![
Path::new("excluded.py").to_path_buf(),
Path::new("**/migrations").to_path_buf()
exclude: None,
extend_exclude: Some(vec![
"excluded.py".to_string(),
"migrations".to_string(),
"directory/also_excluded.py".to_string(),
]),
select: Some(BTreeSet::from([
CheckCode::E402,
CheckCode::E501,
CheckCode::F401,
CheckCode::F403,
CheckCode::F541,
CheckCode::F631,
CheckCode::F634,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
CheckCode::R001,
CheckCode::R002,
])),
select: None,
ignore: None,
}
);

3
src/python.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod builtins;
pub mod future;
pub mod typing;

View File

@@ -156,8 +156,8 @@ pub const BUILTINS: &[&str] = &[
// Globally defined names which are not attributes of the builtins module, or are only present on
// some platforms.
pub const MAGIC_GLOBALS: &[&str] = &[
"__file__",
"__builtins__",
"__annotations__",
"WindowsError",
"__annotations__",
"__builtins__",
"__file__",
];

13
src/python/future.rs Normal file
View File

@@ -0,0 +1,13 @@
/// A copy of `__future__.all_feature_names`.
pub const ALL_FEATURE_NAMES: &[&str] = &[
"nested_scopes",
"generators",
"division",
"absolute_import",
"with_statement",
"print_function",
"unicode_literals",
"barry_as_FLUFL",
"generator_stop",
"annotations",
];

89
src/python/typing.rs Normal file
View File

@@ -0,0 +1,89 @@
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
static ANNOTATED_SUBSCRIPTS: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"AbstractAsyncContextManager",
"AbstractContextManager",
"AbstractSet",
"AsyncContextManager",
"AsyncGenerator",
"AsyncIterable",
"AsyncIterator",
"Awaitable",
"BinaryIO",
"BsdDbShelf",
"ByteString",
"Callable",
"ChainMap",
"ClassVar",
"Collection",
"Concatenate",
"Container",
"ContextManager",
"Coroutine",
"Counter",
"Counter",
"DbfilenameShelf",
"DefaultDict",
"Deque",
"Dict",
"Field",
"Final",
"FrozenSet",
"Generator",
"Iterator",
"Generic",
"IO",
"ItemsView",
"Iterable",
"Iterator",
"KeysView",
"LifoQueue",
"List",
"Mapping",
"MappingProxyType",
"MappingView",
"Match",
"MutableMapping",
"MutableSequence",
"MutableSet",
"Optional",
"OrderedDict",
"PathLike",
"Pattern",
"PriorityQueue",
"Protocol",
"Queue",
"Reversible",
"Sequence",
"Set",
"Shelf",
"SimpleQueue",
"TextIO",
"Tuple",
"Type",
"TypeGuard",
"Union",
"ValuesView",
"WeakKeyDictionary",
"WeakMethod",
"WeakSet",
"WeakValueDictionary",
"cached_property",
"defaultdict",
"deque",
"dict",
"frozenset",
"list",
"partialmethod",
"set",
"tuple",
"type",
])
});
pub fn is_annotated_subscript(name: &str) -> bool {
ANNOTATED_SUBSCRIPTS.contains(name)
}

View File

@@ -1,20 +1,73 @@
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::Path;
use std::path::{Path, PathBuf};
use anyhow::Result;
use glob::Pattern;
use once_cell::sync::Lazy;
use crate::checks::CheckCode;
use crate::checks::{CheckCode, DEFAULT_CHECK_CODES};
use crate::fs;
use crate::pyproject::load_config;
#[derive(Debug, Clone)]
pub enum FilePattern {
Simple(&'static str),
Complex(Pattern, Option<Pattern>),
}
impl FilePattern {
pub fn from_user(pattern: &str, project_root: &Option<PathBuf>) -> Self {
let path = Path::new(pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
};
let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern.");
let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) {
Some(Pattern::new(pattern).expect("Invalid pattern."))
} else {
None
};
FilePattern::Complex(absolute, basename)
}
}
#[derive(Debug)]
pub struct Settings {
pub pyproject: Option<PathBuf>,
pub project_root: Option<PathBuf>,
pub line_length: usize,
pub exclude: Vec<Pattern>,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub select: BTreeSet<CheckCode>,
}
impl Settings {
pub fn for_rule(check_code: CheckCode) -> Self {
Self {
pyproject: None,
project_root: None,
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([check_code]),
}
}
pub fn for_rules(check_codes: Vec<CheckCode>) -> Self {
Self {
pyproject: None,
project_root: None,
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from_iter(check_codes),
}
}
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
self.line_length.hash(state);
@@ -24,45 +77,72 @@ impl Hash for Settings {
}
}
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
FilePattern::Simple(".bzr"),
FilePattern::Simple(".direnv"),
FilePattern::Simple(".eggs"),
FilePattern::Simple(".git"),
FilePattern::Simple(".hg"),
FilePattern::Simple(".mypy_cache"),
FilePattern::Simple(".nox"),
FilePattern::Simple(".pants.d"),
FilePattern::Simple(".ruff_cache"),
FilePattern::Simple(".svn"),
FilePattern::Simple(".tox"),
FilePattern::Simple(".venv"),
FilePattern::Simple("__pypackages__"),
FilePattern::Simple("_build"),
FilePattern::Simple("buck-out"),
FilePattern::Simple("build"),
FilePattern::Simple("dist"),
FilePattern::Simple("node_modules"),
FilePattern::Simple("venv"),
]
});
impl Settings {
pub fn from_paths<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<Self> {
let (project_root, config) = load_config(paths)?;
Ok(Settings {
pub fn from_pyproject(pyproject: Option<PathBuf>, project_root: Option<PathBuf>) -> Self {
let config = load_config(&pyproject);
let mut settings = Settings {
line_length: config.line_length.unwrap_or(88),
exclude: config
.exclude
.unwrap_or_default()
.into_iter()
.map(|path| {
if path.is_relative() {
project_root.join(path)
} else {
path
}
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect()
})
.map(|path| Pattern::new(&path.to_string_lossy()).expect("Invalid pattern."))
.collect(),
select: config.select.unwrap_or_else(|| {
BTreeSet::from([
CheckCode::E402,
CheckCode::E501,
CheckCode::F401,
CheckCode::F403,
CheckCode::F541,
CheckCode::F631,
CheckCode::F634,
CheckCode::F706,
CheckCode::F707,
CheckCode::F823,
CheckCode::F831,
CheckCode::F901,
])
}),
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
extend_exclude: config
.extend_exclude
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect()
})
.unwrap_or_default(),
select: if let Some(select) = config.select {
BTreeSet::from_iter(select)
} else {
BTreeSet::from_iter(DEFAULT_CHECK_CODES)
},
pyproject,
project_root,
};
if let Some(ignore) = &config.ignore {
settings.ignore(ignore);
}
settings
}
pub fn clear(&mut self) {
self.select.clear();
}
pub fn select(&mut self, codes: Vec<CheckCode>) {
self.select.clear();
for code in codes {
self.select.insert(code);
}

View File

@@ -0,0 +1,10 @@
---
source: src/linter.rs
expression: checks
---
- kind: ModuleImportNotAtTopOfFile
location:
row: 20
column: 1
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind:
LineTooLong:
- 123
- 88
location:
row: 5
column: 89
fix: ~

View File

@@ -0,0 +1,53 @@
---
source: src/linter.rs
expression: checks
---
- kind:
NoneComparison: Eq
location:
row: 2
column: 11
fix: ~
- kind:
NoneComparison: NotEq
location:
row: 5
column: 11
fix: ~
- kind:
NoneComparison: Eq
location:
row: 8
column: 4
fix: ~
- kind:
NoneComparison: NotEq
location:
row: 11
column: 4
fix: ~
- kind:
NoneComparison: Eq
location:
row: 14
column: 14
fix: ~
- kind:
NoneComparison: NotEq
location:
row: 17
column: 14
fix: ~
- kind:
NoneComparison: NotEq
location:
row: 20
column: 4
fix: ~
- kind:
NoneComparison: Eq
location:
row: 23
column: 4
fix: ~

View File

@@ -0,0 +1,77 @@
---
source: src/linter.rs
expression: checks
---
- kind:
TrueFalseComparison:
- true
- Eq
location:
row: 2
column: 11
fix: ~
- kind:
TrueFalseComparison:
- false
- NotEq
location:
row: 5
column: 11
fix: ~
- kind:
TrueFalseComparison:
- true
- NotEq
location:
row: 8
column: 4
fix: ~
- kind:
TrueFalseComparison:
- false
- Eq
location:
row: 11
column: 4
fix: ~
- kind:
TrueFalseComparison:
- true
- Eq
location:
row: 14
column: 14
fix: ~
- kind:
TrueFalseComparison:
- false
- NotEq
location:
row: 17
column: 14
fix: ~
- kind:
TrueFalseComparison:
- true
- Eq
location:
row: 20
column: 20
fix: ~
- kind:
TrueFalseComparison:
- false
- Eq
location:
row: 20
column: 44
fix: ~
- kind:
TrueFalseComparison:
- true
- Eq
location:
row: 22
column: 5
fix: ~

View File

@@ -0,0 +1,30 @@
---
source: src/linter.rs
expression: checks
---
- kind: NotInTest
location:
row: 2
column: 10
fix: ~
- kind: NotInTest
location:
row: 5
column: 12
fix: ~
- kind: NotInTest
location:
row: 8
column: 10
fix: ~
- kind: NotInTest
location:
row: 11
column: 25
fix: ~
- kind: NotInTest
location:
row: 14
column: 11
fix: ~

View File

@@ -0,0 +1,20 @@
---
source: src/linter.rs
expression: checks
---
- kind: NotIsTest
location:
row: 2
column: 10
fix: ~
- kind: NotIsTest
location:
row: 5
column: 12
fix: ~
- kind: NotIsTest
location:
row: 8
column: 10
fix: ~

View File

@@ -0,0 +1,85 @@
---
source: src/linter.rs
expression: checks
---
- kind: TypeComparison
location:
row: 2
column: 14
fix: ~
- kind: TypeComparison
location:
row: 5
column: 14
fix: ~
- kind: TypeComparison
location:
row: 10
column: 8
fix: ~
- kind: TypeComparison
location:
row: 15
column: 14
fix: ~
- kind: TypeComparison
location:
row: 18
column: 18
fix: ~
- kind: TypeComparison
location:
row: 18
column: 46
fix: ~
- kind: TypeComparison
location:
row: 20
column: 18
fix: ~
- kind: TypeComparison
location:
row: 22
column: 18
fix: ~
- kind: TypeComparison
location:
row: 24
column: 18
fix: ~
- kind: TypeComparison
location:
row: 26
column: 18
fix: ~
- kind: TypeComparison
location:
row: 28
column: 18
fix: ~
- kind: TypeComparison
location:
row: 30
column: 18
fix: ~
- kind: TypeComparison
location:
row: 32
column: 18
fix: ~
- kind: TypeComparison
location:
row: 34
column: 18
fix: ~
- kind: TypeComparison
location:
row: 40
column: 18
fix: ~
- kind: TypeComparison
location:
row: 42
column: 18
fix: ~

View File

@@ -0,0 +1,20 @@
---
source: src/linter.rs
expression: checks
---
- kind: DoNotUseBareExcept
location:
row: 4
column: 1
fix: ~
- kind: DoNotUseBareExcept
location:
row: 11
column: 1
fix: ~
- kind: DoNotUseBareExcept
location:
row: 16
column: 1
fix: ~

View File

@@ -0,0 +1,30 @@
---
source: src/linter.rs
expression: checks
---
- kind: DoNotAssignLambda
location:
row: 2
column: 1
fix: ~
- kind: DoNotAssignLambda
location:
row: 4
column: 1
fix: ~
- kind: DoNotAssignLambda
location:
row: 7
column: 5
fix: ~
- kind: DoNotAssignLambda
location:
row: 12
column: 1
fix: ~
- kind: DoNotAssignLambda
location:
row: 16
column: 1
fix: ~

View File

@@ -0,0 +1,155 @@
---
source: src/linter.rs
expression: checks
---
- kind:
AmbiguousVariableName: l
location:
row: 3
column: 1
fix: ~
- kind:
AmbiguousVariableName: I
location:
row: 4
column: 1
fix: ~
- kind:
AmbiguousVariableName: O
location:
row: 5
column: 1
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 6
column: 1
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 8
column: 4
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 9
column: 5
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 10
column: 5
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 11
column: 5
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 16
column: 5
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 20
column: 8
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 25
column: 5
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 26
column: 5
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 30
column: 5
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 33
column: 9
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 34
column: 9
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 40
column: 8
fix: ~
- kind:
AmbiguousVariableName: I
location:
row: 40
column: 14
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 44
column: 8
fix: ~
- kind:
AmbiguousVariableName: I
location:
row: 44
column: 16
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 48
column: 9
fix: ~
- kind:
AmbiguousVariableName: I
location:
row: 48
column: 14
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 57
column: 16
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 66
column: 20
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 71
column: 1
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 74
column: 5
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
AmbiguousClassName: l
location:
row: 1
column: 1
fix: ~
- kind:
AmbiguousClassName: I
location:
row: 5
column: 1
fix: ~
- kind:
AmbiguousClassName: O
location:
row: 9
column: 1
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
AmbiguousFunctionName: l
location:
row: 1
column: 1
fix: ~
- kind:
AmbiguousFunctionName: I
location:
row: 5
column: 1
fix: ~
- kind:
AmbiguousFunctionName: O
location:
row: 10
column: 5
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnusedImport: functools
location:
row: 3
column: 1
fix: ~
- kind:
UnusedImport: collections.OrderedDict
location:
row: 5
column: 1
fix: ~
- kind:
UnusedImport: logging.handlers
location:
row: 13
column: 1
fix: ~

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

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

View File

@@ -0,0 +1,10 @@
---
source: src/linter.rs
expression: checks
---
- kind: LateFutureImport
location:
row: 7
column: 1
fix: ~

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

@@ -0,0 +1,17 @@
---
source: src/linter.rs
expression: checks
---
- kind:
ImportStarNotPermitted: F634
location:
row: 5
column: 5
fix: ~
- kind:
ImportStarNotPermitted: F634
location:
row: 9
column: 5
fix: ~

View File

@@ -0,0 +1,11 @@
---
source: src/linter.rs
expression: checks
---
- kind:
FutureFeatureNotDefined: non_existent_feature
location:
row: 2
column: 1
fix: ~

View File

@@ -0,0 +1,20 @@
---
source: src/linter.rs
expression: checks
---
- kind: FStringMissingPlaceholders
location:
row: 4
column: 7
fix: ~
- kind: FStringMissingPlaceholders
location:
row: 5
column: 7
fix: ~
- kind: FStringMissingPlaceholders
location:
row: 7
column: 7
fix: ~

View File

@@ -0,0 +1,20 @@
---
source: src/linter.rs
expression: checks
---
- kind: MultiValueRepeatedKeyLiteral
location:
row: 3
column: 6
fix: ~
- kind: MultiValueRepeatedKeyLiteral
location:
row: 9
column: 5
fix: ~
- kind: MultiValueRepeatedKeyLiteral
location:
row: 11
column: 7
fix: ~

View File

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

View File

@@ -0,0 +1,10 @@
---
source: src/linter.rs
expression: checks
---
- kind: TwoStarredExpressions
location:
row: 1
column: 1
fix: ~

View File

@@ -0,0 +1,15 @@
---
source: src/linter.rs
expression: checks
---
- kind: AssertTuple
location:
row: 1
column: 1
fix: ~
- kind: AssertTuple
location:
row: 2
column: 1
fix: ~

View File

@@ -0,0 +1,15 @@
---
source: src/linter.rs
expression: checks
---
- kind: IsLiteral
location:
row: 1
column: 6
fix: ~
- kind: IsLiteral
location:
row: 4
column: 8
fix: ~

View File

@@ -0,0 +1,10 @@
---
source: src/linter.rs
expression: checks
---
- kind: InvalidPrintSyntax
location:
row: 4
column: 1
fix: ~

View File

@@ -0,0 +1,15 @@
---
source: src/linter.rs
expression: checks
---
- kind: IfTuple
location:
row: 1
column: 1
fix: ~
- kind: IfTuple
location:
row: 7
column: 5
fix: ~

View File

@@ -0,0 +1,25 @@
---
source: src/linter.rs
expression: checks
---
- kind: BreakOutsideLoop
location:
row: 4
column: 5
fix: ~
- kind: BreakOutsideLoop
location:
row: 16
column: 5
fix: ~
- kind: BreakOutsideLoop
location:
row: 20
column: 5
fix: ~
- kind: BreakOutsideLoop
location:
row: 23
column: 1
fix: ~

View File

@@ -0,0 +1,25 @@
---
source: src/linter.rs
expression: checks
---
- kind: ContinueOutsideLoop
location:
row: 4
column: 5
fix: ~
- kind: ContinueOutsideLoop
location:
row: 16
column: 5
fix: ~
- kind: ContinueOutsideLoop
location:
row: 20
column: 5
fix: ~
- kind: ContinueOutsideLoop
location:
row: 23
column: 1
fix: ~

View File

@@ -0,0 +1,20 @@
---
source: src/linter.rs
expression: checks
---
- kind: YieldOutsideFunction
location:
row: 6
column: 5
fix: ~
- kind: YieldOutsideFunction
location:
row: 9
column: 1
fix: ~
- kind: YieldOutsideFunction
location:
row: 10
column: 1
fix: ~

View File

@@ -0,0 +1,15 @@
---
source: src/linter.rs
expression: checks
---
- kind: ReturnOutsideFunction
location:
row: 6
column: 5
fix: ~
- kind: ReturnOutsideFunction
location:
row: 9
column: 1
fix: ~

View File

@@ -0,0 +1,20 @@
---
source: src/linter.rs
expression: checks
---
- kind: DefaultExceptNotLast
location:
row: 3
column: 1
fix: ~
- kind: DefaultExceptNotLast
location:
row: 10
column: 1
fix: ~
- kind: DefaultExceptNotLast
location:
row: 19
column: 1
fix: ~

View File

@@ -0,0 +1,11 @@
---
source: src/linter.rs
expression: checks
---
- kind:
ForwardAnnotationSyntaxError: ///
location:
row: 9
column: 13
fix: ~

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