Compare commits

...

79 Commits

Author SHA1 Message Date
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
Charlie Marsh
45db571935 Bump version to 0.0.26 2022-09-05 12:28:27 -04:00
Charlie Marsh
198e5cf27f Implement R002 (NoAssertEquals) (#98) 2022-09-05 12:27:47 -04:00
Charlie Marsh
79b6472c7c Add a note RE Black compat 2022-09-05 12:27:39 -04:00
Charlie Marsh
f902d25dc7 Implement ESLint-style fix for R0205 (#97) 2022-09-05 12:16:06 -04:00
Charlie Marsh
826bdfeb63 Add utility scripts for AST printing (#105) 2022-09-05 09:31:55 -04:00
Charlie Marsh
a3fb0d6c20 Remove custom serialization for Location (#104) 2022-09-04 17:54:45 -04:00
Charlie Marsh
3cf9e3b201 Implement E402 (ModuleImportNotAtTopOfFile) (#102) 2022-09-04 16:20:36 -04:00
Charlie Marsh
533b4e752b Reduce ignores in CPython benchmark 2022-09-04 16:13:35 -04:00
Charlie Marsh
cf45d520e6 Fix failing test 2022-09-04 12:02:21 -04:00
Harutaka Kawamura
b86414dc7a Implement F707 (DefaultExceptNotLast) (#101) 2022-09-04 11:55:06 -04:00
Charlie Marsh
8f6ab8b37a Fix formatting of some rule messages 2022-09-04 09:52:31 -04:00
Harutaka Kawamura
312bfd8d2b Implement F631 (AssertTuple) (#99) 2022-09-04 08:39:49 -04:00
Harutaka Kawamura
e2f46537fd Fix false positive for IfTuple (#96) 2022-09-03 22:56:55 -04:00
Charlie Marsh
97cc30768d Fix typo in enforce_line_too_long 2022-09-03 16:32:15 -04:00
Grachev Mikhail
d580f2eb90 Check for updates (#90) 2022-09-03 16:31:44 -04:00
Dmitry Dygalo
507fecfd9a perf: Avoid Vec<&str> allocation during line length checking (#95) 2022-09-03 16:27:18 -04:00
Charlie Marsh
4319bd1755 Bump version to 0.0.25 2022-09-03 12:09:11 -04:00
Charlie Marsh
6bb6cb1783 Implement F822 (#94) 2022-09-03 12:08:26 -04:00
Charlie Marsh
e9412c9452 Generate a list of supported lint rules (#93) 2022-09-03 11:56:11 -04:00
Charlie Marsh
94faa7f301 Rename resources/test/src to resources/test/fixtures (#92) 2022-09-03 11:49:03 -04:00
Charlie Marsh
5041f6530c Implement R0205 (#91) 2022-09-03 11:33:54 -04:00
Narawit Rakket
3c7716ef27 refactor: run cargo clippy --fix (#88) 2022-09-02 17:01:24 -04:00
Charlie Marsh
26e1f4b6df Bump version to 0.0.24 2022-09-02 10:18:40 -04:00
Charlie Marsh
17c08523dc Remove rogue println 2022-09-02 10:18:12 -04:00
Charlie Marsh
221f4304ad Add support for __all__ export bindings (#87) 2022-09-02 10:17:31 -04:00
Charlie Marsh
c0131e65e5 Avoid putting decorators in the function scope (#86) 2022-09-02 09:13:06 -04:00
Charlie Marsh
bf4722a62f Fix future-to-__future__ typo (#85) 2022-09-02 08:56:26 -04:00
Charlie Marsh
994f5d452c Update .gitignore 2022-09-01 20:32:32 -04:00
Nikita Sobolev
741857cdf9 Use the latest version of actions/checkout (#79) 2022-09-01 13:18:31 -04:00
Ariel Richtman
4f42f51bd2 Add pre-commit hook (#55) 2022-09-01 13:01:28 -04:00
Dmitry Dygalo
5a3092e805 perf: Compile Regex once (#77) 2022-09-01 12:49:29 -04:00
Charlie Marsh
0318406535 Re-sort lint rules 2022-09-01 12:39:25 -04:00
Sekky61
0c99b5aac5 Fix F832 --> F823 typo (#73) 2022-09-01 12:37:34 -04:00
Kian-Meng Ang
b442402b13 Prettify md/yaml files (#74) 2022-09-01 12:36:47 -04:00
Charlie Marsh
ba27e50164 Bump version to 0.0.23 2022-09-01 09:21:43 -04:00
Charlie Marsh
d55651d470 Rename cache to .ruff_cache 2022-09-01 09:19:32 -04:00
Charlie Marsh
0c110bcecf Add some user-visible logging when pyproject.toml fails to parse 2022-09-01 09:18:47 -04:00
Patrick Haller
9bf8a0d0f1 Check if toml file is parsed correctly (#71) 2022-09-01 09:15:07 -04:00
Eric Hagman
888cfeba35 Remove .to_string() from F634 error message (#67) 2022-09-01 09:08:48 -04:00
69 changed files with 4353 additions and 766 deletions

View File

@@ -2,16 +2,16 @@ name: CI
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
jobs:
cargo_build:
name: "cargo build"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -33,7 +33,7 @@ jobs:
name: "cargo fmt"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -55,7 +55,7 @@ jobs:
name: "cargo clippy"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -77,7 +77,7 @@ jobs:
name: "cargo test"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -99,13 +99,13 @@ jobs:
name: "maturin build"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: "3.10"
- run: pip install maturin
- uses: actions/cache@v3
env:

View File

@@ -1,6 +1,8 @@
name: Release
on:
pull_request:
branches: [main]
create:
tags:
- v*
@@ -127,7 +129,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'
@@ -227,9 +229,9 @@ jobs:
os: [ubuntu-latest, macos-latest]
target: [x86_64, aarch64]
python-version:
- '3.7'
- '3.8'
- '3.9'
- "3.7"
- "3.8"
- "3.9"
exclude:
- os: macos-latest
target: aarch64

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# Local cache
.cache
.ruff_cache
resources/test/cpython
###

5
.pre-commit-config.yaml Normal file
View File

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

7
.pre-commit-hooks.yaml Normal file
View File

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

254
Cargo.lock generated
View File

@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
@@ -197,6 +203,12 @@ dependencies = [
"byteorder",
]
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bincode"
version = "1.3.3"
@@ -362,6 +374,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "clap"
version = "3.2.16"
@@ -465,6 +483,15 @@ dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.6"
@@ -550,6 +577,15 @@ dependencies = [
"generic-array 0.14.6",
]
[[package]]
name = "directories"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs"
version = "2.0.2"
@@ -664,12 +700,32 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "fsevent"
version = "0.4.0"
@@ -914,6 +970,17 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.1"
@@ -1084,6 +1151,12 @@ dependencies = [
"twox-hash",
]
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "memchr"
version = "2.5.0"
@@ -1108,6 +1181,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.6.23"
@@ -1260,9 +1342,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.13.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
[[package]]
name = "opaque-debug"
@@ -1311,6 +1393,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "petgraph"
version = "0.6.2"
@@ -1639,9 +1727,24 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi 0.3.9",
]
[[package]]
name = "ruff"
version = "0.0.22"
version = "0.0.33"
dependencies = [
"anyhow",
"bincode",
@@ -1655,21 +1758,37 @@ dependencies = [
"fern",
"filetime",
"glob",
"itertools",
"lazy_static",
"log",
"notify",
"once_cell",
"rayon",
"regex",
"rustpython-parser",
"serde",
"serde_json",
"toml",
"update-informer",
"walkdir",
]
[[package]]
name = "rustls"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1613f6c6990011a4bc559e79aaf28d715f9f729b#1613f6c6990011a4bc559e79aaf28d715f9f729b"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -1678,7 +1797,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1613f6c6990011a4bc559e79aaf28d715f9f729b#1613f6c6990011a4bc559e79aaf28d715f9f729b"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
dependencies = [
"bincode",
"bitflags",
@@ -1695,7 +1814,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1613f6c6990011a4bc559e79aaf28d715f9f729b#1613f6c6990011a4bc559e79aaf28d715f9f729b"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
dependencies = [
"ahash",
"anyhow",
@@ -1743,6 +1862,22 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "semver"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711"
[[package]]
name = "serde"
version = "1.0.143"
@@ -1874,13 +2009,19 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "ssri"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9cec0d388f39fbe79d7aa600e8d38053bf97b1bc8d350da7c0ba800d0f423f2"
dependencies = [
"base64",
"base64 0.10.1",
"digest 0.8.1",
"hex 0.3.2",
"serde",
@@ -2018,6 +2159,21 @@ dependencies = [
"crunchy",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "toml"
version = "0.5.9"
@@ -2095,12 +2251,27 @@ dependencies = [
"unic-common",
]
[[package]]
name = "unicode-bidi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
[[package]]
name = "unicode-normalization"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-xid"
version = "0.2.3"
@@ -2113,6 +2284,56 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eec8e807a365e5c972debc47b8f06d361b37b94cfd18d48f7adc715fb86404dd"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "update-informer"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f154aee470c0882ea0f3b1cc2a46c5f4d24f282655f7b0cec065614fe24c447f"
dependencies = [
"directories",
"semver",
"serde",
"serde_json",
"ureq",
]
[[package]]
name = "ureq"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f"
dependencies = [
"base64 0.13.0",
"chunked_transfer",
"flate2",
"log",
"once_cell",
"rustls",
"serde",
"serde_json",
"url",
"webpki",
"webpki-roots",
]
[[package]]
name = "url"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "value-bag"
version = "1.0.0-alpha.9"
@@ -2240,6 +2461,25 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
dependencies = [
"webpki",
]
[[package]]
name = "wepoll-ffi"
version = "0.1.2"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.22"
version = "0.0.33"
edition = "2021"
[lib]
@@ -18,17 +18,25 @@ common-path = { version = "1.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.17" }
glob = { version = "0.3.0"}
glob = { version = "0.3.0" }
itertools = "0.10.3"
lazy_static = "1.4.0"
log = { version = "0.4.17" }
notify = { version = "4.0.17" }
once_cell = { version = "1.13.1" }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "1613f6c6990011a4bc559e79aaf28d715f9f729b" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "7d21c6923a506e79cc041708d83cef925efd33f4" }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" }
toml = { version = "0.5.9" }
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true }
walkdir = { version = "2.3.2" }
[features]
default = ["update-informer"]
update-informer = ["dep:update-informer"]
[profile.release]
panic = "abort"
lto = "thin"

132
README.md
View File

@@ -17,8 +17,9 @@ An extremely fast Python linter, written in Rust.
- 🐍 Installable via `pip`
- 🤝 Python 3.10 compatibility
- 🛠️ `pyproject.toml` support
- 📦 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired cache semantics
- 👀 [TypeScript](https://www.typescriptlang.org/docs/handbook/configuring-watch.html)-inspired `--watch` semantics
- 📦 [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._
@@ -51,6 +52,16 @@ 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):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.33
hooks:
- id: lint
```
## Configuration
ruff is configurable both via `pyproject.toml` and the command line.
@@ -75,7 +86,7 @@ ruff path/to/code/ --select F401 F403
See `ruff --help` for more:
```shell
ruff
ruff (v0.0.33)
An extremely fast Python linter.
USAGE:
@@ -85,17 +96,85 @@ ARGS:
<FILES>...
OPTIONS:
-e, --exit-zero Exit with status code "0", even upon detecting 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
-e, --exit-zero Exit with status code "0", even upon detecting errors
--exclude <EXCLUDE>... List of file and/or directory patterns to exclude from checks
-f, --fix Attempt to automatically fix lint errors
-h, --help Print help information
--ignore <IGNORE>... List of error codes to ignore
-n, --no-cache Disable cache reads
-q, --quiet Disable all logging (but still exit with status code "1" upon
detecting errors)
--select <SELECT>... List of error codes to enable
-v, --verbose Enable verbose logging
-w, --watch Run in watch mode by re-running whenever files change
```
### 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.
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).
### Parity with Flake8
ruff's goal is to achieve feature-parity with Flake8 when used (1) without any plugins,
(2) alongside Black, and (3) on Python 3 code. (Using Black obviates the need for many of Flake8's
stylistic checks; limiting to Python 3 obviates the need for certain compatibility checks.)
Under those conditions, Flake8 implements about 58 rules, give or take. At time of writing, ruff
implements 28 rules. (Note that these 28 rules likely cover a disproportionate share of errors:
unused imports, undefined variables, etc.)
Of the unimplemented rules, ruff is missing:
- 15 rules related to string `.format` calls.
- 6 rules related to misplaced `yield`, `break`, and `return` statements.
- 3 rules related to syntax errors in doctests and annotations.
- 2 rules related to redefining unused names.
...along with a variety of others that don't fit neatly into any specific category.
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
1. Flake8 supports a wider range of `noqa` patterns, such as per-file ignores defined in `.flake8`.
2. Flake8 has a plugin architecture and supports writing custom lint rules.
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 |
| 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` |
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def |
| E902 | IOError | No such file or directory: `...` |
| F401 | UnusedImport | `...` imported but unused |
| F403 | ImportStarUsage | Unable to detect undefined names |
| 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` |
| F634 | IfTuple | If test is a tuple, which is always `True` |
| 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 |
| F821 | UndefinedName | Undefined name `...` |
| F822 | UndefinedExport | Undefined name `...` in `__all__` |
| F823 | UndefinedLocal | Local variable `...` referenced before assignment |
| F831 | DuplicateArgumentName | Duplicate argument name in function definition |
| F841 | UnusedVariable | Local variable `...` is assigned to but never used |
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` |
| R001 | UselessObjectInheritance | Class `...` inherits from object |
| R002 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead |
## Development
ruff is written in Rust (1.63.0). You'll need to install the [Rust toolchain](https://www.rust-lang.org/tools/install)
@@ -104,7 +183,7 @@ for development.
Assuming you have `cargo` installed, you can run:
```shell
cargo run resources/test/src
cargo run resources/test/fixtures
cargo fmt
cargo clippy
cargo test
@@ -128,49 +207,25 @@ 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 = [
"Lib/ctypes/test/test_numbers.py",
"Lib/dataclasses.py",
"Lib/lib2to3/tests/data/bom.py",
"Lib/lib2to3/tests/data/crlf.py",
"Lib/lib2to3/tests/data/different_encoding.py",
"Lib/lib2to3/tests/data/false_encoding.py",
"Lib/lib2to3/tests/data/py2_test_grammar.py",
"Lib/sqlite3/test/factory.py",
"Lib/sqlite3/test/hooks.py",
"Lib/sqlite3/test/regression.py",
"Lib/sqlite3/test/transactions.py",
"Lib/sqlite3/test/types.py",
"Lib/test/bad_coding2.py",
"Lib/test/badsyntax_3131.py",
"Lib/test/badsyntax_pep3120.py",
"Lib/test/encoded_modules/module_iso_8859_1.py",
"Lib/test/encoded_modules/module_koi8_r.py",
"Lib/test/sortperf.py",
"Lib/test/test_email/torture_test.py",
"Lib/test/test_fstring.py",
"Lib/test/test_genericpath.py",
"Lib/test/test_getopt.py",
"Lib/test/test_grammar.py",
"Lib/test/test_htmlparser.py",
"Lib/test/test_importlib/stubs.py",
"Lib/test/test_importlib/test_files.py",
"Lib/test/test_importlib/test_metadata_api.py",
"Lib/test/test_importlib/test_open.py",
"Lib/test/test_importlib/test_util.py",
"Lib/test/test_named_expressions.py",
"Lib/test/test_patma.py",
"Lib/test/test_peg_generator/__main__.py",
"Lib/test/test_pipes.py",
"Lib/test/test_source_encoding.py",
"Lib/test/test_weakref.py",
"Lib/test/test_webbrowser.py",
"Lib/tkinter/__main__.py",
"Lib/tkinter/test/test_tkinter/test_variables.py",
"Modules/_decimal/libmpdec/literature/fnt.py",
"Modules/_decimal/tests/deccheck.py",
"Tools/c-analyzer/c_parser/parser/_delim.py",
"Tools/i18n/pygettext.py",
"Tools/test2to3/maintest.py",
@@ -215,6 +270,7 @@ hyperfine --ignore-failure --warmup 5 \
```
In order, these evaluate:
- ruff
- Pylint
- PyFlakes

View File

@@ -0,0 +1,47 @@
/// Generate a Markdown-compatible table of supported lint rules.
use ruff::checks::{CheckKind, RejectedCmpop};
fn main() {
let mut check_kinds: Vec<CheckKind> = vec![
CheckKind::AssertTuple,
CheckKind::DefaultExceptNotLast,
CheckKind::DoNotAssignLambda,
CheckKind::DuplicateArgumentName,
CheckKind::FStringMissingPlaceholders,
CheckKind::IOError("...".to_string()),
CheckKind::IfTuple,
CheckKind::ImportStarUsage,
CheckKind::LineTooLong,
CheckKind::ModuleImportNotAtTopOfFile,
CheckKind::MultiValueRepeatedKeyLiteral,
CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
CheckKind::NoAssertEquals,
CheckKind::NoneComparison(RejectedCmpop::Eq),
CheckKind::NotInTest,
CheckKind::NotIsTest,
CheckKind::RaiseNotImplemented,
CheckKind::ReturnOutsideFunction,
CheckKind::TooManyExpressionsInStarredAssignment,
CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
CheckKind::TwoStarredExpressions,
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());
println!("| Code | Name | Message |");
println!("| ---- | ----- | ------- |");
for check_kind in check_kinds {
println!(
"| {} | {} | {} |",
check_kind.code().as_str(),
check_kind.name(),
check_kind.body()
);
}
}

25
examples/print_ast.rs Normal file
View File

@@ -0,0 +1,25 @@
/// Print the AST for a given Python file.
use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, ValueHint};
use rustpython_parser::parser;
use ruff::fs;
#[derive(Debug, Parser)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::FilePath, required = true)]
file: PathBuf,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let contents = fs::read_file(&cli.file)?;
let python_ast = parser::parse_program(&contents, &cli.file.to_string_lossy())?;
println!("{:#?}", python_ast);
Ok(())
}

25
examples/print_tokens.rs Normal file
View File

@@ -0,0 +1,25 @@
/// Print the token stream for a given Python file.
use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, ValueHint};
use rustpython_parser::lexer;
use ruff::fs;
#[derive(Debug, Parser)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::FilePath, required = true)]
file: PathBuf,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let contents = fs::read_file(&cli.file)?;
for (_, tok, _) in lexer::make_tokenizer(&contents).flatten() {
println!("{:#?}", tok);
}
Ok(())
}

28
resources/test/fixtures/E402.py vendored Normal file
View File

@@ -0,0 +1,28 @@
"""Top-level docstring."""
import a
try:
import b
except ImportError:
pass
else:
pass
import c
if x > 0:
import d
else:
import e
y = x + 1
import f
def foo() -> None:
import e
if __name__ == "__main__":
import g

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

@@ -0,0 +1,11 @@
if var == None:
pass
if None != var:
pass
if var is None:
pass
if None is not var:
pass

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

@@ -0,0 +1,14 @@
if var == True:
pass
if False != var:
pass
if var != False != True:
pass
if var is True:
pass
if False is not var:
pass

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

@@ -0,0 +1,7 @@
my_list = [1, 2, 3]
if not num in my_list:
print(num)
my_list = [1, 2, 3]
if num not in my_list:
print(num)

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

@@ -0,0 +1,5 @@
if not user is None:
print(user.name)
if user is not None:
print(user.name)

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

@@ -0,0 +1,6 @@
from typing import Callable, Iterable
a = lambda x: x**2
b = map(lambda x: 2 * x, range(3))
c: Callable = lambda x: x**2
d: Iterable = map(lambda x: 2 * x, range(3))

41
resources/test/fixtures/F401.py vendored Normal file
View File

@@ -0,0 +1,41 @@
from __future__ import all_feature_names
import os
import functools
from datetime import datetime
from collections import (
Counter,
OrderedDict,
namedtuple,
)
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()
z = multiprocessing.pool.ThreadPool()
__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)

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)

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

@@ -0,0 +1,4 @@
assert (False, "x")
assert (False,)
assert ()
assert True

View File

@@ -6,3 +6,5 @@ for _ in range(5):
pass
elif (3, 4):
pass
elif ():
pass

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

@@ -0,0 +1,46 @@
try:
pass
except:
pass
except ValueError:
pass
try:
pass
except:
pass
except ValueError:
pass
finally:
pass
try:
pass
except:
pass
except ValueError:
pass
else:
pass
try:
pass
except:
pass
try:
pass
except ValueError:
pass
except:
pass
try:
pass
except ValueError:
pass
try:
pass
finally:
pass

View File

@@ -33,14 +33,47 @@ 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

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

@@ -0,0 +1,3 @@
a = 1
__all__ = ["a", "b"]

View File

@@ -15,3 +15,13 @@ def baz():
global my_var
global my_dict
my_dict[my_var] += 1
def dec(x):
return x
@dec
def f():
dec = 1
return dec

141
resources/test/fixtures/R001.py vendored Normal file
View File

@@ -0,0 +1,141 @@
class A:
...
class A(object):
...
class A(
object,
):
...
class A(
object,
#
):
...
class A(
#
object,
):
...
class A(
#
object
):
...
class A(
object
#
):
...
class A(
#
object,
#
):
...
class A(
#
object,
#
):
...
class A(
#
object
#
):
...
class A(
#
object
#
):
...
class B(A, object):
...
class B(object, A):
...
class B(
object,
A,
):
...
class B(
A,
object,
):
...
class B(
object,
# Comment on A.
A,
):
...
class B(
# Comment on A.
A,
object,
):
...
def f():
class A(object):
...
class A(
object,
):
...
class A(
object, # )
):
...
class A(
object # )
,
):
...
object = A
class B(object):
...

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

@@ -0,0 +1,3 @@
self.assertEquals (1, 2)
self.assertEquals(1, 2)
self.assertEqual(3, 4)

View File

@@ -2,16 +2,32 @@
line-length = 88
exclude = ["excluded.py", "**/migrations"]
select = [
"E402",
"E501",
"E711",
"E712",
"E713",
"E714",
"E731",
"E902",
"F401",
"F403",
"F541",
"F601",
"F602",
"F621",
"F622",
"F631",
"F634",
"F704",
"F706",
"F707",
"F821",
"F822",
"F823",
"F831",
"F832",
"F841",
"F901",
"R001",
"R002",
]

View File

@@ -1,18 +0,0 @@
import os
import functools
from collections import (
Counter,
OrderedDict,
namedtuple,
)
import multiprocessing.pool
import multiprocessing.process
import logging.config
import logging.handlers
class X:
def a(self) -> "namedtuple":
x = os.environ["1"]
y = Counter()
z = multiprocessing.pool.ThreadPool()

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;

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

@@ -0,0 +1,430 @@
use std::collections::BTreeSet;
use itertools::izip;
use rustpython_parser::ast::{
Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword,
Location, Stmt, Unaryop,
};
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::{Binding, BindingKind, Scope};
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,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
if matches!(op, Unaryop::Not) {
if let ExprKind::Compare { ops, .. } = &operand.node {
match ops[..] {
[Cmpop::In] => {
if check_not_in {
checks.push(Check::new(CheckKind::NotInTest, operand.location));
}
}
[Cmpop::Is] => {
if check_not_is {
checks.push(Check::new(CheckKind::NotIsTest, operand.location));
}
}
_ => {}
}
}
}
checks
}
/// Check UnusedVariable compliance.
pub fn check_unused_variables(scope: &Scope) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
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()),
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
}
}
/// 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)
|| matches!(autofix, 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)
|| matches!(autofix, 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,
) -> 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,
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()),
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,
) -> 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),
comparator.location,
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
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),
comparator.location,
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
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),
comparator.location,
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
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),
comparator.location,
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
comparator.location,
));
}
}
}
}
checks
}
/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance.
pub fn check_starred_expressions(
elts: &[Expr],
location: Location,
check_too_many_expressions: bool,
check_two_starred_expressions: bool,
) -> 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
}

132
src/ast/operations.rs Normal file
View File

@@ -0,0 +1,132 @@
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
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> {
let mut names: Vec<String> = vec![];
fn add_to_names(names: &mut Vec<String>, elts: &[Expr]) {
for elt in elts {
if let ExprKind::Constant {
value: Constant::Str(value),
..
} = &elt.node
{
names.push(value.to_string())
}
}
}
// Grab the existing bound __all__ values.
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());
}
}
}
if let Some(value) = match &stmt.node {
StmtKind::Assign { value, .. } => Some(value),
StmtKind::AnnAssign { value, .. } => value.as_ref(),
StmtKind::AugAssign { value, .. } => Some(value),
_ => None,
} {
match &value.node {
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
add_to_names(&mut names, elts)
}
ExprKind::BinOp { left, right, .. } => {
let mut current_left = left;
let mut current_right = right;
while let Some(elts) = match &current_right.node {
ExprKind::List { elts, .. } => Some(elts),
ExprKind::Tuple { elts, .. } => Some(elts),
_ => None,
} {
add_to_names(&mut names, elts);
match &current_left.node {
ExprKind::BinOp { left, right, .. } => {
current_left = left;
current_right = right;
}
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
add_to_names(&mut names, elts);
break;
}
_ => break,
}
}
}
_ => {}
}
}
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 { .. })
|| matches!(parent.node, 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 { .. })
|| matches!(parent.node, StmtKind::If { .. })
|| matches!(parent.node, StmtKind::With { .. })
{
return true;
}
}
false
}
/// Struct used to efficiently slice source code at (row, column) Locations.
pub struct SourceCodeLocator<'a> {
content: &'a str,
offsets: Vec<usize>,
initialized: bool,
}
impl<'a> SourceCodeLocator<'a> {
pub fn new(content: &'a str) -> Self {
SourceCodeLocator {
content,
offsets: vec![],
initialized: false,
}
}
pub fn slice_source_code(&mut self, location: &Location) -> &'a str {
if !self.initialized {
let mut offset = 0;
for i in self.content.lines() {
self.offsets.push(offset);
offset += i.len();
offset += 1;
}
self.initialized = true;
}
let offset = self.offsets[location.row() - 1] + location.column() - 1;
&self.content[offset..]
}
}

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);
}
}
}
}

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

@@ -0,0 +1,55 @@
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)]
pub enum ScopeKind {
Class,
Function,
Generator,
Module,
}
#[derive(Clone, Debug)]
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>,
}

View File

@@ -4,64 +4,64 @@ 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) {
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,
@@ -72,13 +72,13 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
} => {
visitor.visit_arguments(args);
for expr in decorator_list {
visitor.visit_expr(expr)
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 {
@@ -90,13 +90,13 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
} => {
visitor.visit_arguments(args);
for expr in decorator_list {
visitor.visit_expr(expr)
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 {
@@ -107,33 +107,33 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
..
} => {
for expr in bases {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
for keyword in keywords {
visitor.visit_keyword(keyword)
}
for stmt in body {
visitor.visit_stmt(stmt)
visitor.visit_keyword(keyword);
}
for expr in decorator_list {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
for stmt in body {
visitor.visit_stmt(stmt);
}
}
StmtKind::Return { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
}
StmtKind::Delete { targets } => {
for expr in targets {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
}
StmtKind::Assign { targets, value, .. } => {
visitor.visit_expr(value);
for expr in targets {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
visitor.visit_expr(value)
}
StmtKind::AugAssign { target, op, value } => {
visitor.visit_expr(target);
@@ -146,11 +146,11 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
value,
..
} => {
visitor.visit_expr(target);
visitor.visit_annotation(annotation);
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
visitor.visit_expr(target);
}
StmtKind::For {
target,
@@ -162,10 +162,10 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &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 {
@@ -178,28 +178,28 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &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);
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);
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, .. } => {
@@ -207,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, .. } => {
@@ -215,7 +215,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::Match { subject, cases } => {
@@ -227,10 +227,10 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
StmtKind::Raise { exc, cause } => {
if let Some(expr) = exc {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
};
if let Some(expr) = cause {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
};
}
StmtKind::Try {
@@ -240,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);
if let Some(expr) = msg {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
}
StmtKind::Import { names } => {
@@ -277,12 +277,12 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
}
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)
visitor.visit_expr(expr);
}
}
ExprKind::NamedExpr { target, value } => {
@@ -309,26 +309,26 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
}
ExprKind::Dict { keys, values } => {
for expr in keys {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
}
ExprKind::Set { elts } => {
for expr in elts {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
}
ExprKind::ListComp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(elt);
}
ExprKind::SetComp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(elt);
}
@@ -338,21 +338,21 @@ 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);
visitor.visit_expr(value);
}
ExprKind::GeneratorExp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
visitor.visit_comprehension(comprehension);
}
visitor.visit_expr(elt);
}
ExprKind::Await { value } => visitor.visit_expr(value),
ExprKind::Yield { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
}
ExprKind::YieldFrom { value } => visitor.visit_expr(value),
@@ -366,7 +366,7 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
visitor.visit_cmpop(cmpop);
}
for expr in comparators {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
}
ExprKind::Call {
@@ -387,12 +387,12 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
} => {
visitor.visit_expr(value);
if let Some(expr) = format_spec {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
}
ExprKind::JoinedStr { values } => {
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr);
}
}
ExprKind::Constant { value, .. } => visitor.visit_constant(value),
@@ -438,7 +438,7 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, 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)
@@ -446,7 +446,10 @@ pub fn walk_constant<V: Visitor + ?Sized>(visitor: &mut V, constant: &Constant)
}
}
pub fn walk_comprehension<V: Visitor + ?Sized>(visitor: &mut V, comprehension: &Comprehension) {
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 {
@@ -454,7 +457,10 @@ pub fn walk_comprehension<V: Visitor + ?Sized>(visitor: &mut V, comprehension: &
}
}
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_ {
@@ -467,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);
}
@@ -475,40 +481,40 @@ 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)
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)
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) {
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) {
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);
}
}
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);
@@ -518,13 +524,13 @@ pub fn walk_match_case<V: Visitor + ?Sized>(visitor: &mut V, match_case: &MatchC
}
}
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),
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, .. } => {
@@ -553,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 } => {
@@ -566,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) {}

2
src/autofix.rs Normal file
View File

@@ -0,0 +1,2 @@
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(())
}
}

126
src/autofix/fixes.rs Normal file
View File

@@ -0,0 +1,126 @@
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.
fn to_absolute(relative: &Location, base: &Location) -> Location {
if relative.row() == 1 {
Location::new(
relative.row() + base.row() - 1,
relative.column() + base.column() - 1,
)
} else {
Location::new(relative.row() + base.row() - 1, relative.column())
}
}
/// Generate a fix to remove a base from a ClassDef statement.
pub fn remove_class_def_base(
locator: &mut SourceCodeLocator,
stmt_at: &Location,
expr_at: Location,
bases: &[Expr],
keywords: &[Keyword],
) -> Option<Fix> {
let content = locator.slice_source_code(stmt_at);
// Case 1: `object` is the only base.
if bases.len() == 1 && keywords.is_empty() {
let mut fix_start = None;
let mut fix_end = None;
let mut count: usize = 0;
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
if matches!(tok, Tok::Lpar) {
if count == 0 {
fix_start = Some(to_absolute(&start, stmt_at));
}
count += 1;
}
if matches!(tok, Tok::Rpar) {
count -= 1;
if count == 0 {
fix_end = Some(to_absolute(&end, stmt_at));
break;
}
}
}
return match (fix_start, fix_end) {
(Some(start), Some(end)) => Some(Fix {
content: "".to_string(),
start,
end,
applied: false,
}),
_ => None,
};
}
if bases
.iter()
.map(|node| node.location)
.chain(keywords.iter().map(|node| node.location))
.any(|location| location > expr_at)
{
// Case 2: `object` is _not_ the last node.
let mut fix_start: Option<Location> = None;
let mut fix_end: Option<Location> = None;
let mut seen_comma = false;
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
let start = to_absolute(&start, stmt_at);
if seen_comma {
if matches!(tok, Tok::Newline) {
fix_end = Some(end);
} else {
fix_end = Some(start);
}
break;
}
if start == expr_at {
fix_start = Some(start);
}
if fix_start.is_some() && matches!(tok, Tok::Comma) {
seen_comma = true;
}
}
match (fix_start, fix_end) {
(Some(start), Some(end)) => Some(Fix {
content: "".to_string(),
start,
end,
applied: false,
}),
_ => None,
}
} else {
// Case 3: `object` is the last node, so we have to find the last token that isn't a comma.
let mut fix_start: Option<Location> = None;
let mut fix_end: Option<Location> = None;
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
let start = to_absolute(&start, stmt_at);
let end = to_absolute(&end, stmt_at);
if start == expr_at {
fix_end = Some(end);
break;
}
if matches!(tok, Tok::Comma) {
fix_start = Some(start);
}
}
match (fix_start, fix_end) {
(Some(start), Some(end)) => Some(Fix {
content: "".to_string(),
start,
end,
applied: false,
}),
_ => None,
}
}
}

View File

@@ -1,4 +1,5 @@
use std::collections::hash_map::DefaultHasher;
use std::fs::Metadata;
use std::hash::{Hash, Hasher};
use std::path::Path;
@@ -7,6 +8,7 @@ use filetime::FileTime;
use log::error;
use serde::{Deserialize, Serialize};
use crate::autofix::fixer;
use crate::message::Message;
use crate::settings::Settings;
@@ -60,18 +62,19 @@ impl From<bool> for Mode {
fn from(value: bool) -> Self {
match value {
true => Mode::ReadWrite,
false => Mode::WriteOnly,
false => Mode::None,
}
}
}
fn cache_dir() -> &'static str {
"./.cache"
"./.ruff_cache"
}
fn cache_key(path: &Path, settings: &Settings) -> 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(),
@@ -80,22 +83,28 @@ fn cache_key(path: &Path, settings: &Settings) -> String {
)
}
pub fn get(path: &Path, settings: &Settings, mode: &Mode) -> Option<Vec<Message>> {
pub fn get(
path: &Path,
metadata: &Metadata,
settings: &Settings,
autofix: &fixer::Mode,
mode: &Mode,
) -> Option<Vec<Message>> {
if !mode.allow_read() {
return None;
};
match cacache::read_sync(cache_dir(), cache_key(path, settings)) {
Ok(encoded) => match path.metadata() {
Ok(m) => match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult { metadata, messages }) => {
if FileTime::from_last_modification_time(&m).unix_seconds() == metadata.mtime {
return Some(messages);
}
match cacache::read_sync(cache_dir(), cache_key(path, settings, autofix)) {
Ok(encoded) => match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
metadata: CacheMetadata { mtime },
messages,
}) => {
if FileTime::from_last_modification_time(metadata).unix_seconds() == mtime {
return Some(messages);
}
Err(e) => error!("Failed to deserialize encoded cache entry: {e:?}"),
},
Err(e) => error!("Failed to read metadata from path: {e:?}"),
}
Err(e) => error!("Failed to deserialize encoded cache entry: {e:?}"),
},
Err(EntryNotFound(_, _)) => {}
Err(e) => error!("Failed to read from cache: {e:?}"),
@@ -103,24 +112,29 @@ pub fn get(path: &Path, settings: &Settings, mode: &Mode) -> Option<Vec<Message>
None
}
pub fn set(path: &Path, settings: &Settings, messages: &[Message], mode: &Mode) {
pub fn set(
path: &Path,
metadata: &Metadata,
settings: &Settings,
autofix: &fixer::Mode,
messages: &[Message],
mode: &Mode,
) {
if !mode.allow_write() {
return;
};
if let Ok(metadata) = path.metadata() {
let check_result = CheckResultRef {
metadata: &CacheMetadata {
mtime: FileTime::from_last_modification_time(&metadata).unix_seconds(),
},
messages,
};
if let Err(e) = cacache::write_sync(
cache_dir(),
cache_key(path, settings),
bincode::serialize(&check_result).unwrap(),
) {
error!("Failed to write to cache: {e:?}")
}
let check_result = CheckResultRef {
metadata: &CacheMetadata {
mtime: FileTime::from_last_modification_time(metadata).unix_seconds(),
},
messages,
};
if let Err(e) = cacache::write_sync(
cache_dir(),
cache_key(path, settings, autofix),
bincode::serialize(&check_result).unwrap(),
) {
error!("Failed to write to cache: {e:?}")
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,24 @@ use rustpython_parser::ast::Location;
use crate::checks::{Check, CheckKind};
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 {
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
!(first == "#" && chunks.next().is_none())
} else {
// Single word / no printable chars - no way to make the line shorter
false
}
} else {
false
}
}
pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings) {
let enforce_line_too_ling = settings.select.contains(CheckKind::LineTooLong.code());
let enforce_line_too_long = settings.select.contains(CheckKind::LineTooLong.code());
let mut line_checks = vec![];
let mut ignored = vec![];
@@ -18,16 +34,13 @@ pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings)
}
// Enforce line length.
if enforce_line_too_ling && line.len() > settings.line_length {
let chunks: Vec<&str> = line.split_whitespace().collect();
if !(chunks.len() == 1 || (chunks.len() == 2 && chunks[0] == "#")) {
let check = Check {
kind: CheckKind::LineTooLong,
location: Location::new(row + 1, settings.line_length + 1),
};
if !check.is_inline_ignored(line) {
line_checks.push(check);
}
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);
}
}
}

View File

@@ -1,24 +1,41 @@
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 enum CheckCode {
E402,
E501,
E711,
E712,
E713,
E714,
E731,
E902,
F401,
F403,
F541,
F601,
F602,
F621,
F622,
F631,
F634,
F704,
F706,
F707,
F821,
F822,
F823,
F831,
F832,
F841,
F901,
R001,
R002,
}
impl FromStr for CheckCode {
@@ -26,18 +43,34 @@ impl FromStr for CheckCode {
fn from_str(s: &str) -> Result<Self> {
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),
"E731" => Ok(CheckCode::E731),
"E902" => Ok(CheckCode::E902),
"F401" => Ok(CheckCode::F401),
"F403" => Ok(CheckCode::F403),
"F541" => Ok(CheckCode::F541),
"F601" => Ok(CheckCode::F601),
"F602" => Ok(CheckCode::F602),
"F621" => Ok(CheckCode::F621),
"F622" => Ok(CheckCode::F622),
"F631" => Ok(CheckCode::F631),
"F634" => Ok(CheckCode::F634),
"F704" => Ok(CheckCode::F704),
"F706" => Ok(CheckCode::F706),
"F707" => Ok(CheckCode::F707),
"F821" => Ok(CheckCode::F821),
"F822" => Ok(CheckCode::F822),
"F823" => Ok(CheckCode::F823),
"F831" => Ok(CheckCode::F831),
"F832" => Ok(CheckCode::F832),
"F841" => Ok(CheckCode::F841),
"F901" => Ok(CheckCode::F901),
"R001" => Ok(CheckCode::R001),
"R002" => Ok(CheckCode::R002),
_ => Err(anyhow::anyhow!("Unknown check code: {s}")),
}
}
@@ -46,36 +79,68 @@ impl FromStr for CheckCode {
impl CheckCode {
pub fn as_str(&self) -> &str {
match self {
CheckCode::E402 => "E402",
CheckCode::E501 => "E501",
CheckCode::E711 => "E711",
CheckCode::E712 => "E712",
CheckCode::E713 => "E713",
CheckCode::E714 => "E714",
CheckCode::E731 => "E731",
CheckCode::E902 => "E902",
CheckCode::F401 => "F401",
CheckCode::F403 => "F403",
CheckCode::F541 => "F541",
CheckCode::F601 => "F601",
CheckCode::F602 => "F602",
CheckCode::F621 => "F621",
CheckCode::F622 => "F622",
CheckCode::F631 => "F631",
CheckCode::F634 => "F634",
CheckCode::F704 => "F704",
CheckCode::F706 => "F706",
CheckCode::F707 => "F707",
CheckCode::F821 => "F821",
CheckCode::F822 => "F822",
CheckCode::F823 => "F823",
CheckCode::F831 => "F831",
CheckCode::F832 => "F832",
CheckCode::F841 => "F841",
CheckCode::F901 => "F901",
CheckCode::R001 => "R001",
CheckCode::R002 => "R002",
}
}
/// The source for the check (either the AST, or the physical lines).
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E402 => &LintSource::AST,
CheckCode::E501 => &LintSource::Lines,
CheckCode::E711 => &LintSource::AST,
CheckCode::E712 => &LintSource::AST,
CheckCode::E713 => &LintSource::AST,
CheckCode::E714 => &LintSource::AST,
CheckCode::E731 => &LintSource::AST,
CheckCode::E902 => &LintSource::FileSystem,
CheckCode::F401 => &LintSource::AST,
CheckCode::F403 => &LintSource::AST,
CheckCode::F541 => &LintSource::AST,
CheckCode::F601 => &LintSource::AST,
CheckCode::F602 => &LintSource::AST,
CheckCode::F621 => &LintSource::AST,
CheckCode::F622 => &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::F832 => &LintSource::AST,
CheckCode::F841 => &LintSource::AST,
CheckCode::F901 => &LintSource::AST,
CheckCode::R001 => &LintSource::AST,
CheckCode::R002 => &LintSource::AST,
}
}
}
@@ -84,94 +149,285 @@ 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 {
AssertTuple,
DefaultExceptNotLast,
DoNotAssignLambda,
DuplicateArgumentName,
FStringMissingPlaceholders,
IOError(String),
IfTuple,
ImportStarUsage,
LineTooLong,
ModuleImportNotAtTopOfFile,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable(String),
NoAssertEquals,
NoneComparison(RejectedCmpop),
NotInTest,
NotIsTest,
RaiseNotImplemented,
YieldOutsideFunction,
ReturnOutsideFunction,
UndefinedName(String),
TooManyExpressionsInStarredAssignment,
TrueFalseComparison(bool, RejectedCmpop),
TwoStarredExpressions,
UndefinedExport(String),
UndefinedLocal(String),
UnusedVariable(String),
UndefinedName(String),
UnusedImport(String),
UnusedVariable(String),
UselessObjectInheritance(String),
YieldOutsideFunction,
}
impl CheckKind {
/// The name of the check.
pub fn name(&self) -> &'static str {
match self {
CheckKind::AssertTuple => "AssertTuple",
CheckKind::DefaultExceptNotLast => "DefaultExceptNotLast",
CheckKind::DuplicateArgumentName => "DuplicateArgumentName",
CheckKind::FStringMissingPlaceholders => "FStringMissingPlaceholders",
CheckKind::IOError(_) => "IOError",
CheckKind::IfTuple => "IfTuple",
CheckKind::ImportStarUsage => "ImportStarUsage",
CheckKind::LineTooLong => "LineTooLong",
CheckKind::DoNotAssignLambda => "DoNotAssignLambda",
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::TooManyExpressionsInStarredAssignment => {
"TooManyExpressionsInStarredAssignment"
}
CheckKind::TrueFalseComparison(_, _) => "TrueFalseComparison",
CheckKind::TwoStarredExpressions => "TwoStarredExpressions",
CheckKind::UndefinedExport(_) => "UndefinedExport",
CheckKind::UndefinedLocal(_) => "UndefinedLocal",
CheckKind::UndefinedName(_) => "UndefinedName",
CheckKind::UnusedImport(_) => "UnusedImport",
CheckKind::UnusedVariable(_) => "UnusedVariable",
CheckKind::UselessObjectInheritance(_) => "UselessObjectInheritance",
CheckKind::YieldOutsideFunction => "YieldOutsideFunction",
}
}
/// A four-letter shorthand code for the check.
pub fn code(&self) -> &'static CheckCode {
match self {
CheckKind::AssertTuple => &CheckCode::F631,
CheckKind::DefaultExceptNotLast => &CheckCode::F707,
CheckKind::DuplicateArgumentName => &CheckCode::F831,
CheckKind::FStringMissingPlaceholders => &CheckCode::F541,
CheckKind::IOError(_) => &CheckCode::E902,
CheckKind::IfTuple => &CheckCode::F634,
CheckKind::ImportStarUsage => &CheckCode::F403,
CheckKind::LineTooLong => &CheckCode::E501,
CheckKind::DoNotAssignLambda => &CheckCode::E731,
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::YieldOutsideFunction => &CheckCode::F704,
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
CheckKind::TooManyExpressionsInStarredAssignment => &CheckCode::F621,
CheckKind::TrueFalseComparison(_, _) => &CheckCode::E712,
CheckKind::TwoStarredExpressions => &CheckCode::F622,
CheckKind::UndefinedExport(_) => &CheckCode::F822,
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
CheckKind::UndefinedName(_) => &CheckCode::F821,
CheckKind::UndefinedLocal(_) => &CheckCode::F832,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::UnusedImport(_) => &CheckCode::F401,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::UselessObjectInheritance(_) => &CheckCode::R001,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
}
}
/// The body text for the check.
pub fn body(&self) -> String {
match self {
CheckKind::AssertTuple => {
"Assert test is a non-empty tuple, which is always `True`".to_string()
}
CheckKind::DefaultExceptNotLast => {
"an `except:` block as not the last exception handler".to_string()
}
CheckKind::DuplicateArgumentName => {
"Duplicate argument name in function definition".to_string()
}
CheckKind::FStringMissingPlaceholders => {
"f-string without any placeholders".to_string()
}
CheckKind::IfTuple => {
"If test is a tuple.to_string(), which is always `True`".to_string()
CheckKind::IOError(name) => {
format!("No such file or directory: `{name}`")
}
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::RaiseNotImplemented => {
"'raise NotImplemented' should be 'raise NotImplementedError".to_string()
CheckKind::DoNotAssignLambda => {
"Do not assign a lambda expression, use a def".to_string()
}
CheckKind::YieldOutsideFunction => {
"a `yield` or `yield from` statement outside of a function/method".to_string()
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::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::UndefinedExport(name) => {
format!("Undefined name `{name}` in `__all__`")
}
CheckKind::UndefinedName(name) => {
format!("Undefined name `{name}`")
}
CheckKind::UnusedVariable(name) => {
format!("Local variable `{name}` is assigned to but never used")
}
CheckKind::UndefinedLocal(name) => {
format!("Local variable `{name}` referenced before assignment")
}
CheckKind::UnusedImport(name) => format!("`{name}` imported but unused"),
CheckKind::UnusedVariable(name) => {
format!("Local variable `{name}` is assigned to but never used")
}
CheckKind::UselessObjectInheritance(name) => {
format!("Class `{name}` inherits from object")
}
CheckKind::YieldOutsideFunction => {
"a `yield` or `yield from` statement outside of a function/method".to_string()
}
}
}
/// 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::IOError(_) => false,
CheckKind::IfTuple => false,
CheckKind::ImportStarUsage => false,
CheckKind::DoNotAssignLambda => false,
CheckKind::LineTooLong => false,
CheckKind::ModuleImportNotAtTopOfFile => false,
CheckKind::MultiValueRepeatedKeyLiteral => false,
CheckKind::MultiValueRepeatedKeyVariable(_) => false,
CheckKind::NoAssertEquals => true,
CheckKind::NotInTest => false,
CheckKind::NotIsTest => false,
CheckKind::NoneComparison(_) => false,
CheckKind::RaiseNotImplemented => false,
CheckKind::ReturnOutsideFunction => false,
CheckKind::TooManyExpressionsInStarredAssignment => false,
CheckKind::TrueFalseComparison(_, _) => false,
CheckKind::TwoStarredExpressions => false,
CheckKind::UndefinedExport(_) => false,
CheckKind::UndefinedLocal(_) => false,
CheckKind::UndefinedName(_) => false,
CheckKind::UnusedImport(_) => false,
CheckKind::UnusedVariable(_) => false,
CheckKind::UselessObjectInheritance(_) => true,
CheckKind::YieldOutsideFunction => false,
}
}
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fix {
pub content: String,
pub start: Location,
pub end: Location,
pub applied: bool,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Check {
pub kind: CheckKind,
pub location: Location,
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 {
kind,
location,
fix: None,
}
}
pub fn amend(&mut self, fix: Fix) {
self.fix = Some(fix);
}
pub fn is_inline_ignored(&self, line: &str) -> bool {
let re = Regex::new(r"(?i)# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?").unwrap();
match re.captures(line) {
match NO_QA_REGEX.captures(line) {
Some(caps) => match caps.name("codes") {
Some(codes) => {
let re = Regex::new(r"[,\s]").unwrap();
for code in re
for code in SPLIT_COMMA_REGEX
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())

View File

@@ -1,4 +1,7 @@
mod builtins;
extern crate core;
mod ast;
mod autofix;
mod cache;
pub mod check_ast;
mod check_lines;
@@ -8,5 +11,5 @@ pub mod linter;
pub mod logging;
pub mod message;
mod pyproject;
mod python;
pub mod settings;
mod visitor;

File diff suppressed because it is too large Load Diff

View File

@@ -6,21 +6,26 @@ use std::time::Instant;
use anyhow::Result;
use clap::{Parser, ValueHint};
use colored::Colorize;
use glob::Pattern;
use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use walkdir::DirEntry;
use ::ruff::checks::CheckCode;
use ::ruff::checks::CheckKind;
use ::ruff::fs::iter_python_files;
use ::ruff::linter::check_path;
use ::ruff::linter::lint_path;
use ::ruff::logging::set_up_logging;
use ::ruff::message::Message;
use ::ruff::settings::Settings;
use ::ruff::tell_user;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Parser)]
#[clap(name = "ruff")]
#[clap(name = format!("{CARGO_PKG_NAME} (v{CARGO_PKG_VERSION})"))]
#[clap(about = "An extremely fast Python linter.", long_about = None)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::AnyPath, required = true)]
@@ -37,21 +42,56 @@ struct Cli {
/// Run in watch mode by re-running whenever files change.
#[clap(short, long, action)]
watch: bool,
/// Attempt to automatically fix lint errors.
#[clap(short, long, action)]
fix: bool,
/// Disable cache reads.
#[clap(short, long, action)]
no_cache: bool,
/// 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.
/// List of error codes to ignore.
#[clap(long, multiple = true)]
ignore: Vec<CheckCode>,
/// List of file and/or directory patterns to exclude from checks.
#[clap(long, multiple = true)]
exclude: Vec<Pattern>,
}
fn run_once(files: &[PathBuf], settings: &Settings, cache: bool) -> Result<Vec<Message>> {
#[cfg(feature = "update-informer")]
fn check_for_updates() {
use update_informer::{registry, Check};
let informer = update_informer::new(registry::PyPI, CARGO_PKG_NAME, CARGO_PKG_VERSION);
if let Some(new_version) = informer.check_version().ok().flatten() {
let msg = format!(
"A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",
pkg_name = CARGO_PKG_NAME.italic().cyan(),
pkg_version = CARGO_PKG_VERSION,
new_version = new_version.to_string().green()
);
let cmd = format!(
"Run to update: {cmd} {pkg_name}",
cmd = "pip3 install --upgrade".green(),
pkg_name = CARGO_PKG_NAME.green()
);
println!("\n{msg}\n{cmd}");
}
}
fn run_once(
files: &[PathBuf],
settings: &Settings,
cache: bool,
autofix: bool,
) -> Result<Vec<Message>> {
// Collect all the files to check.
let start = Instant::now();
let files: Vec<DirEntry> = files
let paths: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude))
.collect();
@@ -59,16 +99,30 @@ fn run_once(files: &[PathBuf], settings: &Settings, cache: bool) -> Result<Vec<M
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let mut messages: Vec<Message> = files
let mut messages: Vec<Message> = paths
.par_iter()
.map(|entry| {
check_path(entry.path(), settings, &cache.into()).unwrap_or_else(|e| {
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();
if settings.select.contains(&CheckCode::E902) {
for file in files {
if !file.exists() {
messages.push(Message {
kind: CheckKind::IOError(file.to_string_lossy().to_string()),
fixed: false,
location: Default::default(),
filename: file.to_string_lossy().to_string(),
})
}
}
}
messages.sort_unstable();
let duration = start.elapsed();
debug!("Checked files in: {:?}", duration);
@@ -77,13 +131,32 @@ fn run_once(files: &[PathBuf], settings: &Settings, cache: bool) -> Result<Vec<M
}
fn report_once(messages: &[Message]) -> Result<()> {
println!("Found {} error(s).", messages.len());
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 !messages.is_empty() {
println!();
for message in messages {
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(())
@@ -119,13 +192,20 @@ fn inner_main() -> Result<ExitCode> {
if !cli.ignore.is_empty() {
settings.ignore(&cli.ignore);
}
if !cli.exclude.is_empty() {
settings.exclude(cli.exclude);
}
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)?;
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
report_continuously(&messages)?;
}
@@ -145,7 +225,7 @@ fn inner_main() -> Result<ExitCode> {
clearscreen::clear()?;
tell_user!("File change detected...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache)?;
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
report_continuously(&messages)?;
}
@@ -156,11 +236,14 @@ fn inner_main() -> Result<ExitCode> {
}
}
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache)?;
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
if !cli.quiet {
report_once(&messages)?;
}
#[cfg(feature = "update-informer")]
check_for_updates();
if !messages.is_empty() && !cli.exit_zero {
return Ok(ExitCode::FAILURE);
}

View File

@@ -7,25 +7,10 @@ use serde::{Deserialize, Serialize};
use crate::checks::CheckKind;
#[derive(Serialize, Deserialize)]
#[serde(remote = "Location")]
struct LocationDef {
#[serde(getter = "Location::row")]
row: usize,
#[serde(getter = "Location::column")]
column: usize,
}
impl From<LocationDef> for Location {
fn from(def: LocationDef) -> Location {
Location::new(def.row, def.column)
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Message {
pub kind: CheckKind,
#[serde(with = "LocationDef")]
pub fixed: bool,
pub location: Location,
pub filename: String,
}
@@ -35,7 +20,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(),
))
}
}

View File

@@ -1,4 +1,3 @@
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use anyhow::Result;
@@ -14,12 +13,20 @@ pub fn load_config<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<(Pat
Some(project_root) => match find_pyproject_toml(&project_root) {
Some(path) => {
debug!("Found pyproject.toml at: {}", path.to_string_lossy());
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default();
Ok((project_root, config))
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())
}
}
}
None => Ok(Default::default()),
},
@@ -32,7 +39,8 @@ pub fn load_config<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<(Pat
pub struct Config {
pub line_length: Option<usize>,
pub exclude: Option<Vec<PathBuf>>,
pub select: Option<BTreeSet<CheckCode>>,
pub select: Option<Vec<CheckCode>>,
pub ignore: Option<Vec<CheckCode>>,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
@@ -82,7 +90,6 @@ 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 anyhow::Result;
@@ -117,6 +124,7 @@ mod tests {
line_length: None,
exclude: None,
select: None,
ignore: None,
})
})
);
@@ -135,6 +143,7 @@ line-length = 79
line_length: Some(79),
exclude: None,
select: None,
ignore: None,
})
})
);
@@ -153,6 +162,7 @@ exclude = ["foo.py"]
line_length: None,
exclude: Some(vec![Path::new("foo.py").to_path_buf()]),
select: None,
ignore: None,
})
})
);
@@ -170,7 +180,27 @@ select = ["E501"]
ruff: Some(Config {
line_length: None,
exclude: None,
select: Some(BTreeSet::from([CheckCode::E501])),
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,
select: None,
ignore: Some(vec![CheckCode::E501]),
})
})
);
@@ -208,18 +238,17 @@ other-attribute = 1
#[test]
fn find_and_parse_pyproject_toml() -> Result<()> {
let project_root = find_project_root([Path::new("resources/test/src/__init__.py")])
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/src"));
assert_eq!(project_root, Path::new("resources/test/fixtures"));
let path = find_pyproject_toml(&project_root).expect("Unable to find pyproject.toml.");
assert_eq!(path, Path::new("resources/test/src/pyproject.toml"));
assert_eq!(path, Path::new("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject
.tool
.map(|tool| tool.ruff)
.flatten()
.and_then(|tool| tool.ruff)
.expect("Unable to find tool.ruff.");
assert_eq!(
config,
@@ -229,20 +258,37 @@ other-attribute = 1
Path::new("excluded.py").to_path_buf(),
Path::new("**/migrations").to_path_buf()
]),
select: Some(BTreeSet::from([
select: Some(vec![
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E731,
CheckCode::E902,
CheckCode::F401,
CheckCode::F403,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F634,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F832,
CheckCode::F841,
CheckCode::F901,
])),
CheckCode::R001,
CheckCode::R002,
]),
ignore: None,
}
);

2
src/python.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod builtins;
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__",
];

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

@@ -0,0 +1,88 @@
use lazy_static::lazy_static;
use std::collections::BTreeSet;
lazy_static! {
static ref ANNOTATED_SUBSCRIPTS: BTreeSet<&'static str> = 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

@@ -27,7 +27,7 @@ impl Hash for Settings {
impl Settings {
pub fn from_paths<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<Self> {
let (project_root, config) = load_config(paths)?;
Ok(Settings {
let mut settings = Settings {
line_length: config.line_length.unwrap_or(88),
exclude: config
.exclude
@@ -42,20 +42,44 @@ impl Settings {
})
.map(|path| Pattern::new(&path.to_string_lossy()).expect("Invalid pattern."))
.collect(),
select: config.select.unwrap_or_else(|| {
BTreeSet::from([
select: BTreeSet::from_iter(config.select.unwrap_or_else(|| {
vec![
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E731,
CheckCode::E902,
CheckCode::F401,
CheckCode::F403,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F634,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F832,
CheckCode::F841,
CheckCode::F901,
])
}),
})
// Disable refactoring codes by default.
// CheckCode::R001,
// CheckCode::R002,
]
})),
};
if let Some(ignore) = &config.ignore {
settings.ignore(ignore);
}
Ok(settings)
}
pub fn select(&mut self, codes: Vec<CheckCode>) {
@@ -70,4 +94,8 @@ impl Settings {
self.select.remove(code);
}
}
pub fn exclude(&mut self, exclude: Vec<Pattern>) {
self.exclude = exclude;
}
}