Compare commits

..

34 Commits

Author SHA1 Message Date
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
52 changed files with 2421 additions and 556 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

@@ -227,9 +227,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.26
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

252
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.23"
version = "0.0.26"
dependencies = [
"anyhow",
"bincode",
@@ -1657,19 +1760,33 @@ dependencies = [
"glob",
"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=ff6112d7c70729be76eaad8c9e5b8ef24d13de99#ff6112d7c70729be76eaad8c9e5b8ef24d13de99"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -1678,7 +1795,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=ff6112d7c70729be76eaad8c9e5b8ef24d13de99#ff6112d7c70729be76eaad8c9e5b8ef24d13de99"
dependencies = [
"bincode",
"bitflags",
@@ -1695,7 +1812,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=ff6112d7c70729be76eaad8c9e5b8ef24d13de99#ff6112d7c70729be76eaad8c9e5b8ef24d13de99"
dependencies = [
"ahash",
"anyhow",
@@ -1743,6 +1860,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 +2007,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 +2157,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 +2249,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 +2282,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 +2459,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.23"
version = "0.0.26"
edition = "2021"
[lib]
@@ -18,15 +18,17 @@ 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" }
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 = "ff6112d7c70729be76eaad8c9e5b8ef24d13de99" }
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"] }
walkdir = { version = "2.3.2" }
[profile.release]

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.26
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.26)
An extremely fast Python linter.
USAGE:
@@ -86,6 +97,7 @@ ARGS:
OPTIONS:
-e, --exit-zero Exit with status code "0", even upon detecting errors
-f, --fix Attempt to automatically fix lint errors
-h, --help Print help information
--ignore <IGNORE>... Comma-separated list of error codes to ignore
-n, --no-cache Disable cache reads
@@ -96,6 +108,37 @@ OPTIONS:
-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).
## Rules
| Code | Name | Message |
| ---- | ----- | ------- |
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file |
| E501 | LineTooLong | Line too long |
| F401 | UnusedImport | `...` imported but unused |
| F403 | ImportStarUsage | Unable to detect undefined names |
| F541 | FStringMissingPlaceholders | f-string without any placeholders |
| 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 +147,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
@@ -131,46 +174,22 @@ Add this `pyproject.toml` to the CPython directory:
[tool.linter]
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 +234,7 @@ hyperfine --ignore-failure --warmup 5 \
```
In order, these evaluate:
- ruff
- Pylint
- PyFlakes

View File

@@ -0,0 +1,37 @@
/// Generate a Markdown-compatible table of supported lint rules.
use ruff::checks::CheckKind;
fn main() {
let mut check_kinds: Vec<CheckKind> = vec![
CheckKind::AssertTuple,
CheckKind::DefaultExceptNotLast,
CheckKind::DuplicateArgumentName,
CheckKind::FStringMissingPlaceholders,
CheckKind::IfTuple,
CheckKind::ImportStarUsage,
CheckKind::LineTooLong,
CheckKind::ModuleImportNotAtTopOfFile,
CheckKind::NoAssertEquals,
CheckKind::RaiseNotImplemented,
CheckKind::ReturnOutsideFunction,
CheckKind::UndefinedExport("...".to_string()),
CheckKind::UndefinedLocal("...".to_string()),
CheckKind::UndefinedName("...".to_string()),
CheckKind::UnusedImport("...".to_string()),
CheckKind::UnusedVariable("...".to_string()),
CheckKind::UselessObjectInheritance("...".to_string()),
CheckKind::YieldOutsideFunction,
];
check_kinds.sort_by_key(|check_kind| check_kind.code());
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

View File

@@ -1,3 +1,4 @@
from __future__ import all_feature_names
import os
import functools
from collections import (
@@ -10,9 +11,15 @@ import multiprocessing.process
import logging.config
import logging.handlers
from blah import ClassA, ClassB, ClassC
class X:
def a(self) -> "namedtuple":
x = os.environ["1"]
y = Counter()
z = multiprocessing.pool.ThreadPool()
__all__ = ["ClassA"] + ["ClassB"]
__all__ += ["ClassC"]

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

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,22 @@
line-length = 88
exclude = ["excluded.py", "**/migrations"]
select = [
"E402",
"E501",
"F401",
"F403",
"F541",
"F631",
"F634",
"F704",
"F706",
"F707",
"F821",
"F822",
"F823",
"F831",
"F832",
"F841",
"F901",
"R001",
"R002",
]

148
src/ast_ops.rs Normal file
View File

@@ -0,0 +1,148 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
fn id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
pub enum ScopeKind {
Class,
Function,
Generator,
Module,
}
pub struct Scope {
pub id: usize,
pub kind: ScopeKind,
pub values: BTreeMap<String, Binding>,
}
impl Scope {
pub fn new(kind: ScopeKind) -> Self {
Scope {
id: id(),
kind,
values: BTreeMap::new(),
}
}
}
#[derive(Clone, Debug)]
pub enum BindingKind {
Argument,
Assignment,
Builtin,
ClassDefinition,
Definition,
Export(Vec<String>),
FutureImportation,
Importation(String),
StarImportation,
SubmoduleImportation(String),
}
#[derive(Clone, Debug)]
pub struct Binding {
pub kind: BindingKind,
pub location: Location,
pub used: Option<usize>,
}
/// 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
}
/// 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..]
}
}

216
src/autofix.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::apply_fixes;
use crate::checks::Fix;
#[test]
fn empty_file() -> Result<()> {
let mut fixes = vec![];
let actual = apply_fixes(fixes.iter_mut(), "");
let expected = "";
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn apply_single_replacement() -> Result<()> {
let mut fixes = vec![Fix {
content: "Bar".to_string(),
start: Location::new(1, 9),
end: Location::new(1, 15),
applied: false,
}];
let actual = apply_fixes(
fixes.iter_mut(),
"class A(object):
...
",
);
let expected = "class A(Bar):
...
";
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn apply_single_removal() -> Result<()> {
let mut fixes = vec![Fix {
content: "".to_string(),
start: Location::new(1, 8),
end: Location::new(1, 16),
applied: false,
}];
let actual = apply_fixes(
fixes.iter_mut(),
"class A(object):
...
",
);
let expected = "class A:
...
";
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn apply_double_removal() -> Result<()> {
let mut fixes = vec![
Fix {
content: "".to_string(),
start: Location::new(1, 8),
end: Location::new(1, 17),
applied: false,
},
Fix {
content: "".to_string(),
start: Location::new(1, 17),
end: Location::new(1, 24),
applied: false,
},
];
let actual = apply_fixes(
fixes.iter_mut(),
"class A(object, object):
...
",
);
let expected = "class A:
...
";
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn ignore_overlapping_fixes() -> Result<()> {
let mut fixes = vec![
Fix {
content: "".to_string(),
start: Location::new(1, 8),
end: Location::new(1, 16),
applied: false,
},
Fix {
content: "ignored".to_string(),
start: Location::new(1, 10),
end: Location::new(1, 12),
applied: false,
},
];
let actual = apply_fixes(
fixes.iter_mut(),
"class A(object):
...
",
);
let expected = "class A:
...
";
assert_eq!(actual, expected);
Ok(())
}
}

View File

@@ -1,7 +1,9 @@
use std::collections::hash_map::DefaultHasher;
use std::fs::Metadata;
use std::hash::{Hash, Hasher};
use std::path::Path;
use crate::autofix;
use cacache::Error::EntryNotFound;
use filetime::FileTime;
use log::error;
@@ -69,9 +71,10 @@ fn cache_dir() -> &'static str {
"./.ruff_cache"
}
fn cache_key(path: &Path, settings: &Settings) -> String {
fn cache_key(path: &Path, settings: &Settings, autofix: &autofix::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: &autofix::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: &autofix::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:?}")
}
}

View File

@@ -1,5 +1,5 @@
use std::collections::{BTreeMap, BTreeSet};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::collections::BTreeSet;
use std::path::Path;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
@@ -7,81 +7,50 @@ use rustpython_parser::ast::{
};
use rustpython_parser::parser;
use crate::ast_ops::{
extract_all_names, Binding, BindingKind, Scope, ScopeKind, SourceCodeLocator,
};
use crate::builtins::{BUILTINS, MAGIC_GLOBALS};
use crate::check_ast::ScopeKind::{Class, Function, Generator, Module};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::settings::Settings;
use crate::visitor;
use crate::visitor::{walk_excepthandler, Visitor};
fn id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
enum ScopeKind {
Class,
Function,
Generator,
Module,
}
struct Scope {
id: usize,
kind: ScopeKind,
values: BTreeMap<String, Binding>,
}
impl Scope {
fn new(kind: ScopeKind) -> Self {
Scope {
id: id(),
kind,
values: BTreeMap::new(),
}
}
}
#[derive(Clone)]
enum BindingKind {
Argument,
Assignment,
Definition,
ClassDefinition,
Builtin,
FutureImportation,
Importation(String),
StarImportation,
SubmoduleImportation(String),
}
#[derive(Clone)]
struct Binding {
kind: BindingKind,
location: Location,
used: Option<usize>,
}
use crate::{autofix, fixer, visitor};
struct Checker<'a> {
locator: SourceCodeLocator<'a>,
settings: &'a Settings,
autofix: &'a autofix::Mode,
path: &'a str,
checks: Vec<Check>,
scopes: Vec<Scope>,
dead_scopes: Vec<Scope>,
deferred: Vec<String>,
in_f_string: bool,
in_annotation: bool,
seen_non_import: bool,
seen_docstring: bool,
}
impl Checker<'_> {
pub fn new(settings: &Settings) -> Checker {
pub fn new<'a>(
settings: &'a Settings,
autofix: &'a autofix::Mode,
path: &'a str,
content: &'a str,
) -> Checker<'a> {
Checker {
settings,
autofix,
path,
locator: SourceCodeLocator::new(content),
checks: vec![],
scopes: vec![],
dead_scopes: vec![],
deferred: vec![],
in_f_string: false,
in_annotation: false,
seen_non_import: false,
seen_docstring: false,
}
}
}
@@ -109,18 +78,24 @@ impl Visitor for Checker<'_> {
}
}
}
StmtKind::FunctionDef { name, .. } => {
self.add_binding(
name.to_string(),
Binding {
kind: BindingKind::Definition,
used: None,
location: stmt.location,
},
);
self.push_scope(Scope::new(Function));
StmtKind::FunctionDef {
name,
decorator_list,
returns,
..
}
StmtKind::AsyncFunctionDef { name, .. } => {
| StmtKind::AsyncFunctionDef {
name,
decorator_list,
returns,
..
} => {
for expr in decorator_list {
self.visit_expr(expr, Some(stmt));
}
for expr in returns {
self.visit_annotation(expr);
}
self.add_binding(
name.to_string(),
Binding {
@@ -129,7 +104,7 @@ impl Visitor for Checker<'_> {
location: stmt.location,
},
);
self.push_scope(Scope::new(Function));
self.push_scope(Scope::new(ScopeKind::Function));
}
StmtKind::Return { .. } => {
if self
@@ -139,19 +114,87 @@ impl Visitor for Checker<'_> {
{
if let Some(scope) = self.scopes.last() {
match scope.kind {
Class | Module => {
self.checks.push(Check {
kind: CheckKind::ReturnOutsideFunction,
location: stmt.location,
});
ScopeKind::Class | ScopeKind::Module => {
self.checks.push(Check::new(
CheckKind::ReturnOutsideFunction,
stmt.location,
));
}
_ => {}
}
}
}
}
StmtKind::ClassDef { .. } => self.push_scope(Scope::new(Class)),
StmtKind::ClassDef {
name,
bases,
keywords,
decorator_list,
..
} => {
if self.settings.select.contains(&CheckCode::R001) {
for expr in bases {
if let ExprKind::Name { id, .. } = &expr.node {
if id == "object" {
let scope = self.scopes.last().expect("No current scope found.");
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!(self.autofix, autofix::Mode::Generate)
|| matches!(self.autofix, autofix::Mode::Apply)
{
if let Some(fix) = fixer::remove_class_def_base(
&mut self.locator,
&stmt.location,
expr.location,
bases,
keywords,
) {
check.amend(fix);
}
} else {
}
self.checks.push(check);
}
_ => {}
}
}
}
}
}
for expr in bases {
self.visit_expr(expr, Some(stmt))
}
for keyword in keywords {
self.visit_keyword(keyword)
}
for expr in decorator_list {
self.visit_expr(expr, Some(stmt))
}
self.push_scope(Scope::new(ScopeKind::Class))
}
StmtKind::Import { names } => {
if self
.settings
.select
.contains(CheckKind::ModuleImportNotAtTopOfFile.code())
&& self.seen_non_import
&& stmt.location.column() == 1
{
self.checks.push(Check::new(
CheckKind::ModuleImportNotAtTopOfFile,
stmt.location,
));
}
for alias in names {
if alias.node.name.contains('.') && alias.node.asname.is_none() {
// TODO(charlie): Multiple submodule imports with the same parent module
@@ -189,18 +232,30 @@ impl Visitor for Checker<'_> {
}
}
StmtKind::ImportFrom { names, module, .. } => {
if self
.settings
.select
.contains(CheckKind::ModuleImportNotAtTopOfFile.code())
&& self.seen_non_import
&& stmt.location.column() == 1
{
self.checks.push(Check::new(
CheckKind::ModuleImportNotAtTopOfFile,
stmt.location,
));
}
for alias in names {
let name = alias
.node
.asname
.clone()
.unwrap_or_else(|| alias.node.name.clone());
if let Some("future") = module.as_deref() {
if let Some("__future__") = module.as_deref() {
self.add_binding(
name,
Binding {
kind: BindingKind::FutureImportation,
used: Some(self.scopes.last().expect("No current scope found.").id),
location: stmt.location,
},
@@ -210,7 +265,6 @@ impl Visitor for Checker<'_> {
name,
Binding {
kind: BindingKind::StarImportation,
used: None,
location: stmt.location,
},
@@ -221,10 +275,8 @@ impl Visitor for Checker<'_> {
.select
.contains(CheckKind::ImportStarUsage.code())
{
self.checks.push(Check {
kind: CheckKind::ImportStarUsage,
location: stmt.location,
});
self.checks
.push(Check::new(CheckKind::ImportStarUsage, stmt.location));
}
} else {
let binding = Binding {
@@ -241,11 +293,11 @@ impl Visitor for Checker<'_> {
}
StmtKind::If { test, .. } => {
if self.settings.select.contains(CheckKind::IfTuple.code()) {
if let ExprKind::Tuple { .. } = test.node {
self.checks.push(Check {
kind: CheckKind::IfTuple,
location: stmt.location,
});
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
self.checks
.push(Check::new(CheckKind::IfTuple, stmt.location));
}
}
}
}
@@ -260,19 +312,19 @@ impl Visitor for Checker<'_> {
ExprKind::Call { func, .. } => {
if let ExprKind::Name { id, .. } = &func.node {
if id == "NotImplemented" {
self.checks.push(Check {
kind: CheckKind::RaiseNotImplemented,
location: stmt.location,
});
self.checks.push(Check::new(
CheckKind::RaiseNotImplemented,
stmt.location,
));
}
}
}
ExprKind::Name { id, .. } => {
if id == "NotImplemented" {
self.checks.push(Check {
kind: CheckKind::RaiseNotImplemented,
location: stmt.location,
});
self.checks.push(Check::new(
CheckKind::RaiseNotImplemented,
stmt.location,
));
}
}
_ => {}
@@ -280,7 +332,54 @@ impl Visitor for Checker<'_> {
}
}
}
StmtKind::AugAssign { target, .. } => self.handle_node_load(target),
StmtKind::AugAssign { target, .. } => {
self.seen_non_import = true;
self.handle_node_load(target);
}
StmtKind::Assert { test, .. } => {
self.seen_non_import = true;
if self.settings.select.contains(CheckKind::AssertTuple.code()) {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
self.checks
.push(Check::new(CheckKind::AssertTuple, stmt.location));
}
}
}
}
StmtKind::Try { handlers, .. } => {
if self
.settings
.select
.contains(CheckKind::DefaultExceptNotLast.code())
{
for (idx, handler) in handlers.iter().enumerate() {
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
if type_.is_none() && idx < handlers.len() - 1 {
self.checks.push(Check::new(
CheckKind::DefaultExceptNotLast,
handler.location,
));
}
}
}
}
StmtKind::Expr { value } => {
if !self.seen_docstring {
if let ExprKind::Constant {
value: Constant::Str(_),
..
} = &value.node
{
self.seen_docstring = true;
}
} else {
self.seen_non_import = true;
}
}
StmtKind::Delete { .. } | StmtKind::Assign { .. } | StmtKind::AnnAssign { .. } => {
self.seen_non_import = true;
}
_ => {}
}
@@ -304,10 +403,10 @@ impl Visitor for Checker<'_> {
&& name != "__traceback_supplement__"
&& matches!(binding.kind, BindingKind::Assignment)
{
self.checks.push(Check {
kind: CheckKind::UnusedVariable(name.to_string()),
location: binding.location,
});
self.checks.push(Check::new(
CheckKind::UnusedVariable(name.to_string()),
binding.location,
));
}
}
@@ -332,23 +431,54 @@ impl Visitor for Checker<'_> {
fn visit_annotation(&mut self, expr: &Expr) {
let initial = self.in_annotation;
self.in_annotation = true;
self.visit_expr(expr);
self.visit_expr(expr, None);
self.in_annotation = initial;
}
fn visit_expr(&mut self, expr: &Expr) {
fn visit_expr(&mut self, expr: &Expr, parent: Option<&Stmt>) {
let initial = self.in_f_string;
match &expr.node {
ExprKind::Name { ctx, .. } => match ctx {
ExprContext::Load => self.handle_node_load(expr),
ExprContext::Store => self.handle_node_store(expr),
ExprContext::Store => self.handle_node_store(expr, parent),
ExprContext::Del => self.handle_node_delete(expr),
},
ExprKind::Call { func, .. } => {
if self.settings.select.contains(&CheckCode::R002) {
if let ExprKind::Attribute { value, attr, .. } = &func.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!(self.autofix, autofix::Mode::Generate)
|| matches!(self.autofix, autofix::Mode::Apply)
{
check.amend(Fix {
content: "assertEqual".to_string(),
start: Location::new(
func.location.row(),
func.location.column() + 1,
),
end: Location::new(
func.location.row(),
func.location.column() + 1 + "assertEquals".len(),
),
applied: false,
});
}
self.checks.push(check);
}
}
}
}
}
}
ExprKind::GeneratorExp { .. }
| ExprKind::ListComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::SetComp { .. } => self.push_scope(Scope::new(Generator)),
ExprKind::Lambda { .. } => self.push_scope(Scope::new(Function)),
| ExprKind::SetComp { .. } => self.push_scope(Scope::new(ScopeKind::Generator)),
ExprKind::Lambda { .. } => self.push_scope(Scope::new(ScopeKind::Function)),
ExprKind::Yield { .. } | ExprKind::YieldFrom { .. } => {
let scope = self.scopes.last().expect("No current scope found.");
if self
@@ -358,10 +488,8 @@ impl Visitor for Checker<'_> {
&& matches!(scope.kind, ScopeKind::Class)
|| matches!(scope.kind, ScopeKind::Module)
{
self.checks.push(Check {
kind: CheckKind::YieldOutsideFunction,
location: expr.location,
});
self.checks
.push(Check::new(CheckKind::YieldOutsideFunction, expr.location));
}
}
ExprKind::JoinedStr { values } => {
@@ -374,10 +502,10 @@ impl Visitor for Checker<'_> {
.iter()
.any(|value| matches!(value.node, ExprKind::FormattedValue { .. }))
{
self.checks.push(Check {
kind: CheckKind::FStringMissingPlaceholders,
location: expr.location,
});
self.checks.push(Check::new(
CheckKind::FStringMissingPlaceholders,
expr.location,
));
}
self.in_f_string = true;
}
@@ -413,24 +541,30 @@ impl Visitor for Checker<'_> {
Some(name) => {
let scope = self.scopes.last().expect("No current scope found.");
if scope.values.contains_key(name) {
self.handle_node_store(&Expr::new(
self.handle_node_store(
&Expr::new(
excepthandler.location,
ExprKind::Name {
id: name.to_string(),
ctx: ExprContext::Store,
},
),
None,
);
}
let scope = self.scopes.last().expect("No current scope found.");
let prev_definition = scope.values.get(name).cloned();
self.handle_node_store(
&Expr::new(
excepthandler.location,
ExprKind::Name {
id: name.to_string(),
ctx: ExprContext::Store,
},
));
}
let scope = self.scopes.last().expect("No current scope found.");
let prev_definition = scope.values.get(name).cloned();
self.handle_node_store(&Expr::new(
excepthandler.location,
ExprKind::Name {
id: name.to_string(),
ctx: ExprContext::Store,
},
));
),
None,
);
walk_excepthandler(self, excepthandler);
@@ -438,10 +572,10 @@ impl Visitor for Checker<'_> {
if let Some(binding) = scope.values.remove(name) {
if self.settings.select.contains(&CheckCode::F841) && binding.used.is_none()
{
self.checks.push(Check {
kind: CheckKind::UnusedVariable(name.to_string()),
location: excepthandler.location,
});
self.checks.push(Check::new(
CheckKind::UnusedVariable(name.to_string()),
excepthandler.location,
));
}
}
@@ -479,10 +613,8 @@ impl Visitor for Checker<'_> {
for arg in all_arguments {
let ident = &arg.node.arg;
if idents.contains(ident.as_str()) {
self.checks.push(Check {
kind: CheckKind::DuplicateArgumentName,
location: arg.location,
});
self.checks
.push(Check::new(CheckKind::DuplicateArgumentName, arg.location));
break;
}
idents.insert(ident);
@@ -559,7 +691,7 @@ impl Checker<'_> {
let mut first_iter = true;
let mut in_generators = false;
for scope in self.scopes.iter_mut().rev() {
if matches!(scope.kind, Class) {
if matches!(scope.kind, ScopeKind::Class) {
if id == "__class__" {
return;
} else if !first_iter && !in_generators {
@@ -572,38 +704,41 @@ impl Checker<'_> {
}
first_iter = false;
in_generators = matches!(scope.kind, Generator);
in_generators = matches!(scope.kind, ScopeKind::Generator);
}
if self.settings.select.contains(&CheckCode::F821) {
self.checks.push(Check {
kind: CheckKind::UndefinedName(id.clone()),
location: expr.location,
})
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
expr.location,
))
}
}
}
fn handle_node_store(&mut self, expr: &Expr) {
fn handle_node_store(&mut self, expr: &Expr, parent: Option<&Stmt>) {
if let ExprKind::Name { id, .. } = &expr.node {
if self.settings.select.contains(&CheckCode::F832) {
let current = self.scopes.last().expect("No current scope found.");
if matches!(current.kind, ScopeKind::Function) && !current.values.contains_key(id) {
for scope in self.scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function) || matches!(scope.kind, Module)
{
let used = scope
.values
.get(id)
.map(|binding| binding.used)
.unwrap_or_default();
if let Some(scope_id) = used {
if scope_id == current.id {
self.checks.push(Check {
kind: CheckKind::UndefinedLocal(id.clone()),
location: expr.location,
});
}
let current = self.scopes.last().expect("No current scope found.");
if self.settings.select.contains(&CheckCode::F823)
&& matches!(current.kind, ScopeKind::Function)
&& !current.values.contains_key(id)
{
for scope in self.scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function)
|| matches!(scope.kind, ScopeKind::Module)
{
let used = scope
.values
.get(id)
.map(|binding| binding.used)
.unwrap_or_default();
if let Some(scope_id) = used {
if scope_id == current.id {
self.checks.push(Check::new(
CheckKind::UndefinedLocal(id.clone()),
expr.location,
));
}
}
}
@@ -611,14 +746,36 @@ impl Checker<'_> {
}
// TODO(charlie): Handle alternate binding types (like `Annotation`).
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Assignment,
used: None,
location: expr.location,
},
);
if id == "__all__"
&& matches!(current.kind, ScopeKind::Module)
&& match parent {
None => false,
Some(stmt) => {
matches!(stmt.node, StmtKind::Assign { .. })
|| matches!(stmt.node, StmtKind::AugAssign { .. })
|| matches!(stmt.node, StmtKind::AnnAssign { .. })
}
}
{
// Really need parent here.
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Export(extract_all_names(parent.unwrap(), current)),
used: None,
location: expr.location,
},
);
} else {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Assignment,
used: None,
location: expr.location,
},
);
}
}
}
@@ -628,10 +785,10 @@ impl Checker<'_> {
if current.values.remove(id).is_none()
&& self.settings.select.contains(&CheckCode::F821)
{
self.checks.push(Check {
kind: CheckKind::UndefinedName(id.clone()),
location: expr.location,
})
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
expr.location,
))
}
}
}
@@ -639,24 +796,57 @@ impl Checker<'_> {
fn check_deferred(&mut self, path: &str) {
for value in self.deferred.clone() {
if let Ok(expr) = &parser::parse_expression(&value, path) {
self.visit_expr(expr);
self.visit_expr(expr, None);
}
}
}
fn check_dead_scopes(&mut self) {
if self.settings.select.contains(&CheckCode::F401) {
// TODO(charlie): Handle `__all__`.
for scope in &self.dead_scopes {
for (_, binding) in scope.values.iter().rev() {
if binding.used.is_none() {
if !self.settings.select.contains(&CheckCode::F822)
&& !self.settings.select.contains(&CheckCode::F401)
{
return;
}
for scope in &self.dead_scopes {
let all_binding = scope.values.get("__all__");
let all_names = all_binding.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some(names),
_ => None,
});
if self.settings.select.contains(&CheckCode::F822)
&& !Path::new(self.path).ends_with("__init__.py")
{
if let Some(binding) = all_binding {
if let Some(names) = all_names {
for name in names {
if !scope.values.contains_key(name) {
self.checks.push(Check::new(
CheckKind::UndefinedExport(name.to_string()),
binding.location,
));
}
}
}
}
}
if self.settings.select.contains(&CheckCode::F401) {
for (name, binding) in scope.values.iter().rev() {
let used = binding.used.is_some()
|| all_names
.map(|names| names.contains(name))
.unwrap_or_default();
if !used {
match &binding.kind {
BindingKind::Importation(full_name)
| BindingKind::SubmoduleImportation(full_name) => {
self.checks.push(Check {
kind: CheckKind::UnusedImport(full_name.to_string()),
location: binding.location,
});
self.checks.push(Check::new(
CheckKind::UnusedImport(full_name.to_string()),
binding.location,
));
}
_ => {}
}
@@ -667,9 +857,15 @@ impl Checker<'_> {
}
}
pub fn check_ast(python_ast: &Suite, settings: &Settings, path: &str) -> Vec<Check> {
let mut checker = Checker::new(settings);
checker.push_scope(Scope::new(Module));
pub fn check_ast(
python_ast: &Suite,
content: &str,
settings: &Settings,
autofix: &autofix::Mode,
path: &str,
) -> Vec<Check> {
let mut checker = Checker::new(settings, autofix, path, content);
checker.push_scope(Scope::new(ScopeKind::Module));
checker.bind_builtins();
for stmt in python_ast {

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,31 @@
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,
F401,
F403,
F541,
F631,
F634,
F704,
F706,
F707,
F821,
F822,
F823,
F831,
F832,
F841,
F901,
R001,
R002,
}
impl FromStr for CheckCode {
@@ -26,18 +33,24 @@ impl FromStr for CheckCode {
fn from_str(s: &str) -> Result<Self> {
match s {
"E402" => Ok(CheckCode::E402),
"E501" => Ok(CheckCode::E501),
"F401" => Ok(CheckCode::F401),
"F403" => Ok(CheckCode::F403),
"F541" => Ok(CheckCode::F541),
"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 +59,48 @@ impl FromStr for CheckCode {
impl CheckCode {
pub fn as_str(&self) -> &str {
match self {
CheckCode::E402 => "E402",
CheckCode::E501 => "E501",
CheckCode::F401 => "F401",
CheckCode::F403 => "F403",
CheckCode::F541 => "F541",
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::F401 => &LintSource::AST,
CheckCode::F403 => &LintSource::AST,
CheckCode::F541 => &LintSource::AST,
CheckCode::F631 => &LintSource::AST,
CheckCode::F634 => &LintSource::AST,
CheckCode::F704 => &LintSource::AST,
CheckCode::F706 => &LintSource::AST,
CheckCode::F707 => &LintSource::AST,
CheckCode::F821 => &LintSource::AST,
CheckCode::F822 => &LintSource::AST,
CheckCode::F823 => &LintSource::AST,
CheckCode::F831 => &LintSource::AST,
CheckCode::F832 => &LintSource::AST,
CheckCode::F841 => &LintSource::AST,
CheckCode::F901 => &LintSource::AST,
CheckCode::R001 => &LintSource::AST,
CheckCode::R002 => &LintSource::AST,
}
}
}
@@ -88,42 +113,84 @@ pub enum LintSource {
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CheckKind {
AssertTuple,
DefaultExceptNotLast,
DuplicateArgumentName,
FStringMissingPlaceholders,
IfTuple,
ImportStarUsage,
LineTooLong,
ModuleImportNotAtTopOfFile,
NoAssertEquals,
RaiseNotImplemented,
YieldOutsideFunction,
ReturnOutsideFunction,
UndefinedName(String),
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::IfTuple => "IfTuple",
CheckKind::ImportStarUsage => "ImportStarUsage",
CheckKind::LineTooLong => "LineTooLong",
CheckKind::ModuleImportNotAtTopOfFile => "ModuleImportNotAtTopOfFile",
CheckKind::NoAssertEquals => "NoAssertEquals",
CheckKind::RaiseNotImplemented => "RaiseNotImplemented",
CheckKind::ReturnOutsideFunction => "ReturnOutsideFunction",
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::IfTuple => &CheckCode::F634,
CheckKind::ImportStarUsage => &CheckCode::F403,
CheckKind::LineTooLong => &CheckCode::E501,
CheckKind::ModuleImportNotAtTopOfFile => &CheckCode::E402,
CheckKind::NoAssertEquals => &CheckCode::R002,
CheckKind::RaiseNotImplemented => &CheckCode::F901,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
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()
}
@@ -133,43 +200,103 @@ impl CheckKind {
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::ModuleImportNotAtTopOfFile => {
"Module level import not at top of file".to_string()
}
CheckKind::YieldOutsideFunction => {
"a `yield` or `yield from` statement outside of a function/method".to_string()
CheckKind::NoAssertEquals => {
"`assertEquals` is deprecated, use `assertEqual` instead".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::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::IfTuple => false,
CheckKind::ImportStarUsage => false,
CheckKind::LineTooLong => false,
CheckKind::ModuleImportNotAtTopOfFile => false,
CheckKind::NoAssertEquals => true,
CheckKind::RaiseNotImplemented => false,
CheckKind::ReturnOutsideFunction => false,
CheckKind::UndefinedExport(_) => false,
CheckKind::UndefinedLocal(_) => false,
CheckKind::UndefinedName(_) => false,
CheckKind::UnusedImport(_) => false,
CheckKind::UnusedVariable(_) => false,
CheckKind::UselessObjectInheritance(_) => true,
CheckKind::YieldOutsideFunction => false,
}
}
}
#[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())

126
src/fixer.rs Normal file
View File

@@ -0,0 +1,126 @@
use crate::ast_ops::SourceCodeLocator;
use rustpython_parser::ast::{Expr, Keyword, Location};
use rustpython_parser::lexer;
use rustpython_parser::token::Tok;
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,8 +1,11 @@
mod ast_ops;
mod autofix;
mod builtins;
mod cache;
pub mod check_ast;
mod check_lines;
pub mod checks;
mod fixer;
pub mod fs;
pub mod linter;
pub mod logging;

View File

@@ -4,20 +4,15 @@ use anyhow::Result;
use log::debug;
use rustpython_parser::parser;
use crate::autofix::fix_file;
use crate::check_ast::check_ast;
use crate::check_lines::check_lines;
use crate::checks::{Check, LintSource};
use crate::message::Message;
use crate::settings::Settings;
use crate::{cache, fs};
pub fn check_path(path: &Path, settings: &Settings, mode: &cache::Mode) -> Result<Vec<Message>> {
// Check the cache.
if let Some(messages) = cache::get(path, settings, mode) {
debug!("Cache hit for: {}", path.to_string_lossy());
return Ok(messages);
}
use crate::{autofix, cache, fs};
fn check_path(path: &Path, settings: &Settings, autofix: &autofix::Mode) -> Result<Vec<Check>> {
// Read the file from disk.
let contents = fs::read_file(path)?;
@@ -32,22 +27,51 @@ pub fn check_path(path: &Path, settings: &Settings, mode: &cache::Mode) -> Resul
{
let path = path.to_string_lossy();
let python_ast = parser::parse_program(&contents, &path)?;
checks.extend(check_ast(&python_ast, settings, &path));
checks.extend(check_ast(&python_ast, &contents, settings, autofix, &path));
}
// Run the lines-based checks.
check_lines(&mut checks, &contents, settings);
Ok(checks)
}
pub fn lint_path(
path: &Path,
settings: &Settings,
mode: &cache::Mode,
autofix: &autofix::Mode,
) -> Result<Vec<Message>> {
let metadata = path.metadata()?;
// Check the cache.
if let Some(messages) = cache::get(path, &metadata, settings, autofix, mode) {
debug!("Cache hit for: {}", path.to_string_lossy());
return Ok(messages);
}
// Read the file from disk.
let contents = fs::read_file(path)?;
// Generate checks.
let mut checks = check_path(path, settings, autofix)?;
// Apply autofix.
if matches!(autofix, autofix::Mode::Apply) {
fix_file(&mut checks, &contents, path)?;
};
// Convert to messages.
let messages: Vec<Message> = checks
.into_iter()
.map(|check| Message {
kind: check.kind,
fixed: check.fix.map(|fix| fix.applied).unwrap_or_default(),
location: check.location,
filename: path.to_string_lossy().to_string(),
})
.collect();
cache::set(path, settings, &messages, mode);
cache::set(path, &metadata, settings, autofix, &messages, mode);
Ok(messages)
}
@@ -60,26 +84,49 @@ mod tests {
use anyhow::Result;
use rustpython_parser::ast::Location;
use crate::checks::{CheckCode, CheckKind};
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::linter::check_path;
use crate::message::Message;
use crate::{cache, settings};
use crate::{autofix, settings};
#[test]
fn e402() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/E402.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E402]),
},
&autofix::Mode::Generate,
)?;
let expected = vec![Check {
kind: CheckKind::ModuleImportNotAtTopOfFile,
location: Location::new(20, 1),
fix: None,
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn e501() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/E501.py"),
Path::new("./resources/test/fixtures/E501.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E501]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![Message {
let expected = vec![Check {
kind: CheckKind::LineTooLong,
location: Location::new(5, 89),
filename: "./resources/test/src/E501.py".to_string(),
fix: None,
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
@@ -92,29 +139,29 @@ mod tests {
#[test]
fn f401() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F401.py"),
Path::new("./resources/test/fixtures/F401.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F401]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![
Message {
Check {
kind: CheckKind::UnusedImport("logging.handlers".to_string()),
location: Location::new(11, 1),
filename: "./resources/test/src/F401.py".to_string(),
location: Location::new(12, 1),
fix: None,
},
Message {
Check {
kind: CheckKind::UnusedImport("functools".to_string()),
location: Location::new(2, 1),
filename: "./resources/test/src/F401.py".to_string(),
},
Message {
kind: CheckKind::UnusedImport("collections.OrderedDict".to_string()),
location: Location::new(3, 1),
filename: "./resources/test/src/F401.py".to_string(),
fix: None,
},
Check {
kind: CheckKind::UnusedImport("collections.OrderedDict".to_string()),
location: Location::new(4, 1),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
@@ -128,24 +175,24 @@ mod tests {
#[test]
fn f403() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F403.py"),
Path::new("./resources/test/fixtures/F403.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F403]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![
Message {
Check {
kind: CheckKind::ImportStarUsage,
location: Location::new(1, 1),
filename: "./resources/test/src/F403.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::ImportStarUsage,
location: Location::new(2, 1),
filename: "./resources/test/src/F403.py".to_string(),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
@@ -158,29 +205,60 @@ mod tests {
#[test]
fn f541() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F541.py"),
Path::new("./resources/test/fixtures/F541.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F541]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![
Message {
Check {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(4, 7),
filename: "./resources/test/src/F541.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(5, 7),
filename: "./resources/test/src/F541.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(7, 7),
filename: "./resources/test/src/F541.py".to_string(),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f631() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/F631.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F631]),
},
&autofix::Mode::Generate,
)?;
let expected = vec![
Check {
kind: CheckKind::AssertTuple,
location: Location::new(1, 1),
fix: None,
},
Check {
kind: CheckKind::AssertTuple,
location: Location::new(2, 1),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
@@ -194,24 +272,24 @@ mod tests {
#[test]
fn f634() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F634.py"),
Path::new("./resources/test/fixtures/F634.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F634]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![
Message {
Check {
kind: CheckKind::IfTuple,
location: Location::new(1, 1),
filename: "./resources/test/src/F634.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::IfTuple,
location: Location::new(7, 5),
filename: "./resources/test/src/F634.py".to_string(),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
@@ -225,29 +303,29 @@ mod tests {
#[test]
fn f704() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F704.py"),
Path::new("./resources/test/fixtures/F704.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F704]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![
Message {
Check {
kind: CheckKind::YieldOutsideFunction,
location: Location::new(6, 5),
filename: "./resources/test/src/F704.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::YieldOutsideFunction,
location: Location::new(9, 1),
filename: "./resources/test/src/F704.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::YieldOutsideFunction,
location: Location::new(10, 1),
filename: "./resources/test/src/F704.py".to_string(),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
@@ -261,24 +339,60 @@ mod tests {
#[test]
fn f706() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F706.py"),
Path::new("./resources/test/fixtures/F706.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F706]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![
Message {
Check {
kind: CheckKind::ReturnOutsideFunction,
location: Location::new(6, 5),
filename: "./resources/test/src/F706.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::ReturnOutsideFunction,
location: Location::new(9, 1),
filename: "./resources/test/src/F706.py".to_string(),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f707() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/F707.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F707]),
},
&autofix::Mode::Generate,
)?;
let expected = vec![
Check {
kind: CheckKind::DefaultExceptNotLast,
location: Location::new(3, 1),
fix: None,
},
Check {
kind: CheckKind::DefaultExceptNotLast,
location: Location::new(10, 1),
fix: None,
},
Check {
kind: CheckKind::DefaultExceptNotLast,
location: Location::new(19, 1),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
@@ -292,34 +406,34 @@ mod tests {
#[test]
fn f821() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F821.py"),
Path::new("./resources/test/fixtures/F821.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F821]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![
Message {
Check {
kind: CheckKind::UndefinedName("self".to_string()),
location: Location::new(2, 12),
filename: "./resources/test/src/F821.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::UndefinedName("self".to_string()),
location: Location::new(6, 13),
filename: "./resources/test/src/F821.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::UndefinedName("self".to_string()),
location: Location::new(10, 9),
filename: "./resources/test/src/F821.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::UndefinedName("numeric_string".to_string()),
location: Location::new(21, 12),
filename: "./resources/test/src/F821.py".to_string(),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
@@ -330,32 +444,80 @@ mod tests {
Ok(())
}
#[test]
fn f822() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/F822.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F822]),
},
&autofix::Mode::Generate,
)?;
let expected = vec![Check {
kind: CheckKind::UndefinedExport("b".to_string()),
location: Location::new(3, 1),
fix: None,
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f823() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/F823.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F823]),
},
&autofix::Mode::Generate,
)?;
let expected = vec![Check {
kind: CheckKind::UndefinedLocal("my_var".to_string()),
location: Location::new(6, 5),
fix: None,
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f831() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F831.py"),
Path::new("./resources/test/fixtures/F831.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F831]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![
Message {
Check {
kind: CheckKind::DuplicateArgumentName,
location: Location::new(1, 25),
filename: "./resources/test/src/F831.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::DuplicateArgumentName,
location: Location::new(5, 28),
filename: "./resources/test/src/F831.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::DuplicateArgumentName,
location: Location::new(9, 27),
filename: "./resources/test/src/F831.py".to_string(),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
@@ -366,51 +528,27 @@ mod tests {
Ok(())
}
#[test]
fn f832() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F832.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F832]),
},
&cache::Mode::None,
)?;
let expected = vec![Message {
kind: CheckKind::UndefinedLocal("my_var".to_string()),
location: Location::new(6, 5),
filename: "./resources/test/src/F832.py".to_string(),
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f841() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F841.py"),
Path::new("./resources/test/fixtures/F841.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F841]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![
Message {
Check {
kind: CheckKind::UnusedVariable("e".to_string()),
location: Location::new(3, 1),
filename: "./resources/test/src/F841.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::UnusedVariable("z".to_string()),
location: Location::new(16, 5),
filename: "./resources/test/src/F841.py".to_string(),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
@@ -424,24 +562,286 @@ mod tests {
#[test]
fn f901() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F901.py"),
Path::new("./resources/test/fixtures/F901.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F901]),
},
&cache::Mode::None,
&autofix::Mode::Generate,
)?;
let expected = vec![
Message {
Check {
kind: CheckKind::RaiseNotImplemented,
location: Location::new(2, 5),
filename: "./resources/test/src/F901.py".to_string(),
fix: None,
},
Message {
Check {
kind: CheckKind::RaiseNotImplemented,
location: Location::new(6, 5),
filename: "./resources/test/src/F901.py".to_string(),
fix: None,
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn r001() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/R001.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::R001]),
},
&autofix::Mode::Generate,
)?;
let expected = vec![
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(5, 9),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(5, 8),
end: Location::new(5, 16),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(10, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(9, 8),
end: Location::new(11, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(16, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(15, 8),
end: Location::new(18, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(24, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(22, 8),
end: Location::new(25, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(31, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(29, 8),
end: Location::new(32, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(37, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(36, 8),
end: Location::new(39, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(45, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(43, 8),
end: Location::new(47, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(53, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(51, 8),
end: Location::new(55, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(61, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(59, 8),
end: Location::new(63, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(69, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(67, 8),
end: Location::new(71, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("B".to_string()),
location: Location::new(75, 12),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(75, 10),
end: Location::new(75, 18),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("B".to_string()),
location: Location::new(79, 9),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(79, 9),
end: Location::new(79, 17),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("B".to_string()),
location: Location::new(84, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(84, 5),
end: Location::new(85, 5),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("B".to_string()),
location: Location::new(92, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(91, 6),
end: Location::new(92, 11),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("B".to_string()),
location: Location::new(98, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(98, 5),
end: Location::new(100, 5),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("B".to_string()),
location: Location::new(108, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(107, 6),
end: Location::new(108, 11),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(114, 13),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(114, 12),
end: Location::new(114, 20),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(119, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(118, 8),
end: Location::new(120, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(125, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(124, 8),
end: Location::new(126, 2),
applied: false,
}),
},
Check {
kind: CheckKind::UselessObjectInheritance("A".to_string()),
location: Location::new(131, 5),
fix: Some(Fix {
content: "".to_string(),
start: Location::new(130, 8),
end: Location::new(133, 2),
applied: false,
}),
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn r002() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/R002.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::R002]),
},
&autofix::Mode::Generate,
)?;
let expected = vec![
Check {
kind: CheckKind::NoAssertEquals,
location: Location::new(1, 19),
fix: Some(Fix {
content: "assertEqual".to_string(),
start: Location::new(1, 6),
end: Location::new(1, 18),
applied: false,
}),
},
Check {
kind: CheckKind::NoAssertEquals,
location: Location::new(2, 18),
fix: Some(Fix {
content: "assertEqual".to_string(),
start: Location::new(2, 6),
end: Location::new(2, 18),
applied: false,
}),
},
];
assert_eq!(actual.len(), expected.len());

View File

@@ -13,14 +13,17 @@ use walkdir::DirEntry;
use ::ruff::checks::CheckCode;
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,6 +40,9 @@ 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,
@@ -48,7 +54,12 @@ struct Cli {
ignore: Vec<CheckCode>,
}
fn run_once(files: &[PathBuf], settings: &Settings, cache: bool) -> Result<Vec<Message>> {
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
@@ -62,7 +73,7 @@ fn run_once(files: &[PathBuf], settings: &Settings, cache: bool) -> Result<Vec<M
let mut messages: Vec<Message> = files
.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![]
})
@@ -77,13 +88,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(())
@@ -121,11 +151,15 @@ fn inner_main() -> Result<ExitCode> {
}
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 +179,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 +190,13 @@ 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)?;
}
check_for_updates();
if !messages.is_empty() && !cli.exit_zero {
return Ok(ExitCode::FAILURE);
}
@@ -169,6 +205,29 @@ fn inner_main() -> Result<ExitCode> {
Ok(ExitCode::SUCCESS)
}
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 main() -> ExitCode {
match inner_main() {
Ok(code) => code,

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,
}

View File

@@ -216,18 +216,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,
@@ -238,18 +237,24 @@ other-attribute = 1
Path::new("**/migrations").to_path_buf()
]),
select: Some(BTreeSet::from([
CheckCode::E402,
CheckCode::E501,
CheckCode::F401,
CheckCode::F403,
CheckCode::F541,
CheckCode::F631,
CheckCode::F634,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F832,
CheckCode::F841,
CheckCode::F901,
CheckCode::R001,
CheckCode::R002,
])),
}
);

View File

@@ -44,14 +44,17 @@ impl Settings {
.collect(),
select: config.select.unwrap_or_else(|| {
BTreeSet::from([
CheckCode::E402,
CheckCode::E501,
CheckCode::F401,
CheckCode::F403,
CheckCode::F541,
CheckCode::F631,
CheckCode::F634,
CheckCode::F706,
CheckCode::F707,
CheckCode::F823,
CheckCode::F831,
CheckCode::F832,
CheckCode::F901,
])
}),

View File

@@ -11,7 +11,7 @@ pub trait Visitor {
fn visit_annotation(&mut self, expr: &Expr) {
walk_expr(self, expr);
}
fn visit_expr(&mut self, expr: &Expr) {
fn visit_expr(&mut self, expr: &Expr, _parent: Option<&Stmt>) {
walk_expr(self, expr);
}
fn visit_constant(&mut self, constant: &Constant) {
@@ -63,82 +63,43 @@ pub trait Visitor {
pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
match &stmt.node {
StmtKind::FunctionDef {
args,
body,
decorator_list,
returns,
..
} => {
StmtKind::FunctionDef { args, body, .. } => {
visitor.visit_arguments(args);
for expr in decorator_list {
visitor.visit_expr(expr)
}
for expr in returns {
visitor.visit_annotation(expr);
}
for stmt in body {
visitor.visit_stmt(stmt)
}
}
StmtKind::AsyncFunctionDef {
args,
body,
decorator_list,
returns,
..
} => {
StmtKind::AsyncFunctionDef { args, body, .. } => {
visitor.visit_arguments(args);
for expr in decorator_list {
visitor.visit_expr(expr)
}
for expr in returns {
visitor.visit_annotation(expr);
}
for stmt in body {
visitor.visit_stmt(stmt)
}
}
StmtKind::ClassDef {
bases,
keywords,
body,
decorator_list,
..
} => {
for expr in bases {
visitor.visit_expr(expr)
}
for keyword in keywords {
visitor.visit_keyword(keyword)
}
StmtKind::ClassDef { body, .. } => {
for stmt in body {
visitor.visit_stmt(stmt)
}
for expr in decorator_list {
visitor.visit_expr(expr)
}
}
StmtKind::Return { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::Delete { targets } => {
for expr in targets {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::Assign { targets, value, .. } => {
for expr in targets {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
visitor.visit_expr(value)
visitor.visit_expr(value, Some(stmt))
}
StmtKind::AugAssign { target, op, value } => {
visitor.visit_expr(target);
visitor.visit_expr(target, Some(stmt));
visitor.visit_operator(op);
visitor.visit_expr(value);
visitor.visit_expr(value, Some(stmt));
}
StmtKind::AnnAssign {
target,
@@ -146,10 +107,10 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
value,
..
} => {
visitor.visit_expr(target);
visitor.visit_expr(target, Some(stmt));
visitor.visit_annotation(annotation);
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::For {
@@ -159,8 +120,8 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
orelse,
..
} => {
visitor.visit_expr(target);
visitor.visit_expr(iter);
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(iter, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -175,8 +136,8 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
orelse,
..
} => {
visitor.visit_expr(target);
visitor.visit_expr(iter);
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(iter, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -185,7 +146,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
}
StmtKind::While { test, body, orelse } => {
visitor.visit_expr(test);
visitor.visit_expr(test, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -194,7 +155,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
}
StmtKind::If { test, body, orelse } => {
visitor.visit_expr(test);
visitor.visit_expr(test, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -220,17 +181,17 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
StmtKind::Match { subject, cases } => {
// TODO(charlie): Handle `cases`.
visitor.visit_expr(subject);
visitor.visit_expr(subject, Some(stmt));
for match_case in cases {
visitor.visit_match_case(match_case);
}
}
StmtKind::Raise { exc, cause } => {
if let Some(expr) = exc {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
};
if let Some(expr) = cause {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
};
}
StmtKind::Try {
@@ -253,9 +214,9 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
}
StmtKind::Assert { test, msg } => {
visitor.visit_expr(test);
visitor.visit_expr(test, None);
if let Some(expr) = msg {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::Import { names } => {
@@ -270,7 +231,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
StmtKind::Global { .. } => {}
StmtKind::Nonlocal { .. } => {}
StmtKind::Expr { value } => visitor.visit_expr(value),
StmtKind::Expr { value } => visitor.visit_expr(value, Some(stmt)),
StmtKind::Pass => {}
StmtKind::Break => {}
StmtKind::Continue => {}
@@ -282,55 +243,55 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
ExprKind::BoolOp { op, values } => {
visitor.visit_boolop(op);
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::NamedExpr { target, value } => {
visitor.visit_expr(target);
visitor.visit_expr(value);
visitor.visit_expr(target, None);
visitor.visit_expr(value, None);
}
ExprKind::BinOp { left, op, right } => {
visitor.visit_expr(left);
visitor.visit_expr(left, None);
visitor.visit_operator(op);
visitor.visit_expr(right);
visitor.visit_expr(right, None);
}
ExprKind::UnaryOp { op, operand } => {
visitor.visit_unaryop(op);
visitor.visit_expr(operand);
visitor.visit_expr(operand, None);
}
ExprKind::Lambda { args, body } => {
visitor.visit_arguments(args);
visitor.visit_expr(body);
visitor.visit_expr(body, None);
}
ExprKind::IfExp { test, body, orelse } => {
visitor.visit_expr(test);
visitor.visit_expr(body);
visitor.visit_expr(orelse);
visitor.visit_expr(test, None);
visitor.visit_expr(body, None);
visitor.visit_expr(orelse, None);
}
ExprKind::Dict { keys, values } => {
for expr in keys {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::Set { elts } => {
for expr in elts {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::ListComp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(elt);
visitor.visit_expr(elt, None);
}
ExprKind::SetComp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(elt);
visitor.visit_expr(elt, None);
}
ExprKind::DictComp {
key,
@@ -340,33 +301,33 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(key);
visitor.visit_expr(value);
visitor.visit_expr(key, None);
visitor.visit_expr(value, None);
}
ExprKind::GeneratorExp { elt, generators } => {
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(elt);
visitor.visit_expr(elt, None);
}
ExprKind::Await { value } => visitor.visit_expr(value),
ExprKind::Await { value } => visitor.visit_expr(value, None),
ExprKind::Yield { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::YieldFrom { value } => visitor.visit_expr(value),
ExprKind::YieldFrom { value } => visitor.visit_expr(value, None),
ExprKind::Compare {
left,
ops,
comparators,
} => {
visitor.visit_expr(left);
visitor.visit_expr(left, None);
for cmpop in ops {
visitor.visit_cmpop(cmpop);
}
for expr in comparators {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::Call {
@@ -374,9 +335,9 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
args,
keywords,
} => {
visitor.visit_expr(func);
visitor.visit_expr(func, None);
for expr in args {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for keyword in keywords {
visitor.visit_keyword(keyword);
@@ -385,28 +346,28 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
ExprKind::FormattedValue {
value, format_spec, ..
} => {
visitor.visit_expr(value);
visitor.visit_expr(value, None);
if let Some(expr) = format_spec {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::JoinedStr { values } => {
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::Constant { value, .. } => visitor.visit_constant(value),
ExprKind::Attribute { value, ctx, .. } => {
visitor.visit_expr(value);
visitor.visit_expr(value, None);
visitor.visit_expr_context(ctx);
}
ExprKind::Subscript { value, slice, ctx } => {
visitor.visit_expr(value);
visitor.visit_expr(slice);
visitor.visit_expr(value, None);
visitor.visit_expr(slice, None);
visitor.visit_expr_context(ctx);
}
ExprKind::Starred { value, ctx } => {
visitor.visit_expr(value);
visitor.visit_expr(value, None);
visitor.visit_expr_context(ctx);
}
ExprKind::Name { ctx, .. } => {
@@ -414,25 +375,25 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
}
ExprKind::List { elts, ctx } => {
for expr in elts {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
visitor.visit_expr_context(ctx);
}
ExprKind::Tuple { elts, ctx } => {
for expr in elts {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
visitor.visit_expr_context(ctx);
}
ExprKind::Slice { lower, upper, step } => {
if let Some(expr) = lower {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
if let Some(expr) = upper {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
if let Some(expr) = step {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
}
}
@@ -447,10 +408,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) {
visitor.visit_expr(&comprehension.target);
visitor.visit_expr(&comprehension.iter);
visitor.visit_expr(&comprehension.target, None);
visitor.visit_expr(&comprehension.iter, None);
for expr in &comprehension.ifs {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
}
@@ -458,7 +419,7 @@ pub fn walk_excepthandler<V: Visitor + ?Sized>(visitor: &mut V, excepthandler: &
match &excepthandler.node {
ExcepthandlerKind::ExceptHandler { type_, body, .. } => {
if let Some(expr) = type_ {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for stmt in body {
visitor.visit_stmt(stmt);
@@ -481,13 +442,13 @@ pub fn walk_arguments<V: Visitor + ?Sized>(visitor: &mut V, arguments: &Argument
visitor.visit_arg(arg);
}
for expr in &arguments.kw_defaults {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
if let Some(arg) = &arguments.kwarg {
visitor.visit_arg(arg)
}
for expr in &arguments.defaults {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
@@ -498,20 +459,20 @@ pub fn walk_arg<V: Visitor + ?Sized>(visitor: &mut V, arg: &Arg) {
}
pub fn walk_keyword<V: Visitor + ?Sized>(visitor: &mut V, keyword: &Keyword) {
visitor.visit_expr(&keyword.node.value);
visitor.visit_expr(&keyword.node.value, None);
}
pub fn walk_withitem<V: Visitor + ?Sized>(visitor: &mut V, withitem: &Withitem) {
visitor.visit_expr(&withitem.context_expr);
visitor.visit_expr(&withitem.context_expr, None);
if let Some(expr) = &withitem.optional_vars {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
}
pub fn walk_match_case<V: Visitor + ?Sized>(visitor: &mut V, match_case: &MatchCase) {
visitor.visit_pattern(&match_case.pattern);
if let Some(expr) = &match_case.guard {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for stmt in &match_case.body {
visitor.visit_stmt(stmt);
@@ -520,7 +481,7 @@ 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) {
match &pattern.node {
PatternKind::MatchValue { value } => visitor.visit_expr(value),
PatternKind::MatchValue { value } => visitor.visit_expr(value, None),
PatternKind::MatchSingleton { value } => visitor.visit_constant(value),
PatternKind::MatchSequence { patterns } => {
for pattern in patterns {
@@ -529,7 +490,7 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
}
PatternKind::MatchMapping { keys, patterns, .. } => {
for expr in keys {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for pattern in patterns {
visitor.visit_pattern(pattern);
@@ -541,7 +502,7 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
kwd_patterns,
..
} => {
visitor.visit_expr(cls);
visitor.visit_expr(cls, None);
for pattern in patterns {
visitor.visit_pattern(pattern);
}