Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1eeae90f1 | ||
|
|
27025055ee | ||
|
|
e306fe0765 | ||
|
|
5ffb9c08d5 | ||
|
|
1a8940f015 | ||
|
|
45db571935 | ||
|
|
198e5cf27f | ||
|
|
79b6472c7c | ||
|
|
f902d25dc7 | ||
|
|
826bdfeb63 | ||
|
|
a3fb0d6c20 | ||
|
|
3cf9e3b201 | ||
|
|
533b4e752b | ||
|
|
cf45d520e6 | ||
|
|
b86414dc7a | ||
|
|
8f6ab8b37a | ||
|
|
312bfd8d2b | ||
|
|
e2f46537fd | ||
|
|
97cc30768d | ||
|
|
d580f2eb90 | ||
|
|
507fecfd9a |
@@ -1,5 +1,5 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff
|
||||
rev: v0.0.25
|
||||
rev: v0.0.27
|
||||
hooks:
|
||||
- id: lint
|
||||
|
||||
248
Cargo.lock
generated
248
Cargo.lock
generated
@@ -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"
|
||||
@@ -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.25"
|
||||
version = "0.0.27"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@@ -1655,6 +1758,7 @@ dependencies = [
|
||||
"fern",
|
||||
"filetime",
|
||||
"glob",
|
||||
"itertools",
|
||||
"log",
|
||||
"notify",
|
||||
"once_cell",
|
||||
@@ -1664,13 +1768,26 @@ dependencies = [
|
||||
"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",
|
||||
@@ -1679,7 +1796,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",
|
||||
@@ -1696,7 +1813,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",
|
||||
@@ -1744,6 +1861,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"
|
||||
@@ -1875,13 +2008,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",
|
||||
@@ -2019,6 +2158,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"
|
||||
@@ -2096,12 +2250,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"
|
||||
@@ -2114,6 +2283,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"
|
||||
@@ -2241,6 +2460,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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.25"
|
||||
version = "0.0.27"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -18,16 +18,18 @@ common-path = { version = "1.0.0" }
|
||||
dirs = { version = "4.0.0" }
|
||||
fern = { version = "0.6.1" }
|
||||
filetime = { version = "0.2.17" }
|
||||
glob = { version = "0.3.0"}
|
||||
glob = { version = "0.3.0" }
|
||||
itertools = "0.10.3"
|
||||
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]
|
||||
|
||||
58
README.md
58
README.md
@@ -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._
|
||||
@@ -56,7 +57,7 @@ ruff also works with [Pre-Commit](https://pre-commit.com) (requires Cargo on sys
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff
|
||||
rev: v0.0.25
|
||||
rev: v0.0.27
|
||||
hooks:
|
||||
- id: lint
|
||||
```
|
||||
@@ -85,7 +86,7 @@ ruff path/to/code/ --select F401 F403
|
||||
See `ruff --help` for more:
|
||||
|
||||
```shell
|
||||
ruff
|
||||
ruff (v0.0.27)
|
||||
An extremely fast Python linter.
|
||||
|
||||
USAGE:
|
||||
@@ -96,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
|
||||
@@ -106,24 +108,42 @@ 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 |
|
||||
| E711 | NoneComparison | Comparison to `None` should be `cond is None` |
|
||||
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` |
|
||||
| E713 | NotInTest | Test for membership should be `not in` |
|
||||
| E714 | NotIsTest | Test for object identity should be `is not` |
|
||||
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def |
|
||||
| E902 | IOError | No such file or directory: `...` |
|
||||
| F401 | UnusedImport | `...` imported but unused |
|
||||
| F403 | ImportStarUsage | Unable to detect undefined names |
|
||||
| F541 | FStringMissingPlaceholders | f-string without any placeholders |
|
||||
| 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__ |
|
||||
| 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 |
|
||||
| R0205 | UselessObjectInheritance | Class ... inherits from object |
|
||||
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` |
|
||||
| R001 | UselessObjectInheritance | Class `...` inherits from object |
|
||||
| R002 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead |
|
||||
|
||||
## Development
|
||||
|
||||
@@ -160,46 +180,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",
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
/// Generate a Markdown-compatible table of supported lint rules.
|
||||
use ruff::checks::CheckKind;
|
||||
use ruff::checks::{CheckKind, RejectedCmpop};
|
||||
|
||||
fn main() {
|
||||
let mut check_kinds: Vec<CheckKind> = vec![
|
||||
CheckKind::AssertTuple,
|
||||
CheckKind::DefaultExceptNotLast,
|
||||
CheckKind::DoNotAssignLambda,
|
||||
CheckKind::DuplicateArgumentName,
|
||||
CheckKind::FStringMissingPlaceholders,
|
||||
CheckKind::IOError("...".to_string()),
|
||||
CheckKind::IfTuple,
|
||||
CheckKind::ImportStarUsage,
|
||||
CheckKind::LineTooLong,
|
||||
CheckKind::ModuleImportNotAtTopOfFile,
|
||||
CheckKind::NoAssertEquals,
|
||||
CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
CheckKind::NotInTest,
|
||||
CheckKind::NotIsTest,
|
||||
CheckKind::RaiseNotImplemented,
|
||||
CheckKind::ReturnOutsideFunction,
|
||||
CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
|
||||
CheckKind::UndefinedExport("...".to_string()),
|
||||
CheckKind::UndefinedLocal("...".to_string()),
|
||||
CheckKind::UndefinedName("...".to_string()),
|
||||
CheckKind::UndefinedExport("...".to_string()),
|
||||
CheckKind::UnusedImport("...".to_string()),
|
||||
CheckKind::UnusedVariable("...".to_string()),
|
||||
CheckKind::UselessObjectInheritance("...".to_string()),
|
||||
|
||||
25
examples/print_ast.rs
Normal file
25
examples/print_ast.rs
Normal 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
25
examples/print_tokens.rs
Normal 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
28
resources/test/fixtures/E402.py
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Top-level docstring."""
|
||||
import a
|
||||
|
||||
try:
|
||||
import b
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
import c
|
||||
|
||||
if x > 0:
|
||||
import d
|
||||
else:
|
||||
import e
|
||||
|
||||
y = x + 1
|
||||
|
||||
import f
|
||||
|
||||
|
||||
def foo() -> None:
|
||||
import e
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import g
|
||||
11
resources/test/fixtures/E711.py
vendored
Normal file
11
resources/test/fixtures/E711.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
if var == None:
|
||||
pass
|
||||
|
||||
if None != var:
|
||||
pass
|
||||
|
||||
if var is None:
|
||||
pass
|
||||
|
||||
if None is not var:
|
||||
pass
|
||||
14
resources/test/fixtures/E712.py
vendored
Normal file
14
resources/test/fixtures/E712.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
if var == True:
|
||||
pass
|
||||
|
||||
if False != var:
|
||||
pass
|
||||
|
||||
if var != False != True:
|
||||
pass
|
||||
|
||||
if var is True:
|
||||
pass
|
||||
|
||||
if False is not var:
|
||||
pass
|
||||
7
resources/test/fixtures/E713.py
vendored
Normal file
7
resources/test/fixtures/E713.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
my_list = [1, 2, 3]
|
||||
if not num in my_list:
|
||||
print(num)
|
||||
|
||||
my_list = [1, 2, 3]
|
||||
if num not in my_list:
|
||||
print(num)
|
||||
5
resources/test/fixtures/E714.py
vendored
Normal file
5
resources/test/fixtures/E714.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
if not user is None:
|
||||
print(user.name)
|
||||
|
||||
if user is not None:
|
||||
print(user.name)
|
||||
2
resources/test/fixtures/E731.py
vendored
Normal file
2
resources/test/fixtures/E731.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
a = lambda x: x**2
|
||||
b = map(lambda x: 2 * x, range(3))
|
||||
4
resources/test/fixtures/F631.py
vendored
Normal file
4
resources/test/fixtures/F631.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
assert (False, "x")
|
||||
assert (False,)
|
||||
assert ()
|
||||
assert True
|
||||
2
resources/test/fixtures/F634.py
vendored
2
resources/test/fixtures/F634.py
vendored
@@ -6,3 +6,5 @@ for _ in range(5):
|
||||
pass
|
||||
elif (3, 4):
|
||||
pass
|
||||
elif ():
|
||||
pass
|
||||
|
||||
46
resources/test/fixtures/F707.py
vendored
Normal file
46
resources/test/fixtures/F707.py
vendored
Normal 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
|
||||
141
resources/test/fixtures/R001.py
vendored
Normal file
141
resources/test/fixtures/R001.py
vendored
Normal 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
3
resources/test/fixtures/R002.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
self.assertEquals (1, 2)
|
||||
self.assertEquals(1, 2)
|
||||
self.assertEqual(3, 4)
|
||||
22
resources/test/fixtures/R0205.py
vendored
22
resources/test/fixtures/R0205.py
vendored
@@ -1,22 +0,0 @@
|
||||
class A:
|
||||
...
|
||||
|
||||
|
||||
class B(object):
|
||||
...
|
||||
|
||||
|
||||
class C(B, object):
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
class D(object):
|
||||
...
|
||||
|
||||
|
||||
object = A
|
||||
|
||||
|
||||
class E(object):
|
||||
...
|
||||
12
resources/test/fixtures/pyproject.toml
vendored
12
resources/test/fixtures/pyproject.toml
vendored
@@ -2,18 +2,28 @@
|
||||
line-length = 88
|
||||
exclude = ["excluded.py", "**/migrations"]
|
||||
select = [
|
||||
"E402",
|
||||
"E501",
|
||||
"E711",
|
||||
"E712",
|
||||
"E713",
|
||||
"E714",
|
||||
"E731",
|
||||
"E902",
|
||||
"F401",
|
||||
"F403",
|
||||
"F541",
|
||||
"F631",
|
||||
"F634",
|
||||
"F704",
|
||||
"F706",
|
||||
"F707",
|
||||
"F821",
|
||||
"F822",
|
||||
"F823",
|
||||
"F831",
|
||||
"F841",
|
||||
"F901",
|
||||
"R0205",
|
||||
"R001",
|
||||
"R002",
|
||||
]
|
||||
|
||||
@@ -115,3 +115,34 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
|
||||
|
||||
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
216
src/autofix.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
68
src/cache.rs
68
src/cache.rs
@@ -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:?}")
|
||||
}
|
||||
}
|
||||
|
||||
421
src/check_ast.rs
421
src/check_ast.rs
@@ -1,21 +1,26 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
use itertools::izip;
|
||||
use rustpython_parser::ast::{
|
||||
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Stmt,
|
||||
StmtKind, Suite,
|
||||
Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
|
||||
Location, Stmt, StmtKind, Suite, Unaryop,
|
||||
};
|
||||
use rustpython_parser::parser;
|
||||
|
||||
use crate::ast_ops::{extract_all_names, Binding, BindingKind, Scope, ScopeKind};
|
||||
use crate::ast_ops::{
|
||||
extract_all_names, Binding, BindingKind, Scope, ScopeKind, SourceCodeLocator,
|
||||
};
|
||||
use crate::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::checks::{Check, CheckCode, CheckKind, Fix, RejectedCmpop};
|
||||
use crate::settings::Settings;
|
||||
use crate::visitor;
|
||||
use crate::visitor::{walk_excepthandler, Visitor};
|
||||
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>,
|
||||
@@ -23,19 +28,30 @@ struct Checker<'a> {
|
||||
deferred: Vec<String>,
|
||||
in_f_string: bool,
|
||||
in_annotation: bool,
|
||||
seen_non_import: bool,
|
||||
seen_docstring: bool,
|
||||
}
|
||||
|
||||
impl Checker<'_> {
|
||||
pub fn new<'a>(settings: &'a Settings, path: &'a str) -> Checker<'a> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,10 +116,10 @@ impl Visitor for Checker<'_> {
|
||||
if let Some(scope) = self.scopes.last() {
|
||||
match scope.kind {
|
||||
ScopeKind::Class | ScopeKind::Module => {
|
||||
self.checks.push(Check {
|
||||
kind: CheckKind::ReturnOutsideFunction,
|
||||
location: stmt.location,
|
||||
});
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::ReturnOutsideFunction,
|
||||
stmt.location,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -117,7 +133,7 @@ impl Visitor for Checker<'_> {
|
||||
decorator_list,
|
||||
..
|
||||
} => {
|
||||
if self.settings.select.contains(&CheckCode::R0205) {
|
||||
if self.settings.select.contains(&CheckCode::R001) {
|
||||
for expr in bases {
|
||||
if let ExprKind::Name { id, .. } = &expr.node {
|
||||
if id == "object" {
|
||||
@@ -128,12 +144,25 @@ impl Visitor for Checker<'_> {
|
||||
kind: BindingKind::Builtin,
|
||||
..
|
||||
}) => {
|
||||
self.checks.push(Check {
|
||||
kind: CheckKind::UselessObjectInheritance(
|
||||
name.to_string(),
|
||||
),
|
||||
location: stmt.location,
|
||||
});
|
||||
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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -154,6 +183,19 @@ impl Visitor for Checker<'_> {
|
||||
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
|
||||
@@ -191,6 +233,19 @@ 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
|
||||
@@ -221,10 +276,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 +294,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 +313,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 +333,67 @@ 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::Assign { value, .. } => {
|
||||
self.seen_non_import = true;
|
||||
if self
|
||||
.settings
|
||||
.select
|
||||
.contains(CheckKind::DoNotAssignLambda.code())
|
||||
{
|
||||
if let ExprKind::Lambda { .. } = &value.node {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::DoNotAssignLambda, stmt.location));
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::Delete { .. } | StmtKind::AnnAssign { .. } => {
|
||||
self.seen_non_import = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -304,10 +417,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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,6 +457,37 @@ impl Visitor for Checker<'_> {
|
||||
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 { .. }
|
||||
@@ -358,10 +502,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,13 +516,134 @@ 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;
|
||||
}
|
||||
ExprKind::UnaryOp { op, operand } => {
|
||||
if matches!(op, Unaryop::Not) {
|
||||
if let ExprKind::Compare { ops, .. } = &operand.node {
|
||||
match ops[..] {
|
||||
[Cmpop::In] => {
|
||||
if self.settings.select.contains(CheckKind::NotInTest.code()) {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::NotInTest, operand.location));
|
||||
}
|
||||
}
|
||||
[Cmpop::Is] => {
|
||||
if self.settings.select.contains(CheckKind::NotIsTest.code()) {
|
||||
self.checks
|
||||
.push(Check::new(CheckKind::NotIsTest, operand.location));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ExprKind::Compare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
} => {
|
||||
let op = ops.first().unwrap();
|
||||
let comparator = left;
|
||||
|
||||
// Check `left`.
|
||||
if self.settings.select.contains(&CheckCode::E711)
|
||||
&& matches!(
|
||||
comparator.node,
|
||||
ExprKind::Constant {
|
||||
value: Constant::None,
|
||||
kind: None
|
||||
}
|
||||
)
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.select.contains(&CheckCode::E712) {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Bool(value),
|
||||
kind: None,
|
||||
} = comparator.node
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check each comparator in order.
|
||||
for (op, comparator) in izip!(ops, comparators) {
|
||||
if self.settings.select.contains(&CheckCode::E711)
|
||||
&& matches!(
|
||||
comparator.node,
|
||||
ExprKind::Constant {
|
||||
value: Constant::None,
|
||||
kind: None
|
||||
}
|
||||
)
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.select.contains(&CheckCode::E712) {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Bool(value),
|
||||
kind: None,
|
||||
} = comparator.node
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
|
||||
comparator.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ExprKind::Constant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
@@ -444,10 +707,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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,10 +748,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);
|
||||
@@ -582,10 +843,10 @@ impl Checker<'_> {
|
||||
}
|
||||
|
||||
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,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -609,10 +870,10 @@ impl Checker<'_> {
|
||||
.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,
|
||||
});
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::UndefinedLocal(id.clone()),
|
||||
expr.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -659,10 +920,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,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -696,10 +957,10 @@ impl Checker<'_> {
|
||||
if let Some(names) = all_names {
|
||||
for name in names {
|
||||
if !scope.values.contains_key(name) {
|
||||
self.checks.push(Check {
|
||||
kind: CheckKind::UndefinedExport(name.to_string()),
|
||||
location: binding.location,
|
||||
});
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::UndefinedExport(name.to_string()),
|
||||
binding.location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -717,10 +978,10 @@ impl Checker<'_> {
|
||||
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,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -731,8 +992,14 @@ impl Checker<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_ast(python_ast: &Suite, settings: &Settings, path: &str) -> Vec<Check> {
|
||||
let mut checker = Checker::new(settings, path);
|
||||
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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
196
src/checks.rs
196
src/checks.rs
@@ -8,20 +8,30 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord)]
|
||||
pub enum CheckCode {
|
||||
E402,
|
||||
E501,
|
||||
E711,
|
||||
E712,
|
||||
E713,
|
||||
E714,
|
||||
E731,
|
||||
E902,
|
||||
F401,
|
||||
F403,
|
||||
F541,
|
||||
F631,
|
||||
F634,
|
||||
F704,
|
||||
F706,
|
||||
F707,
|
||||
F821,
|
||||
F822,
|
||||
F823,
|
||||
F831,
|
||||
F841,
|
||||
F901,
|
||||
R0205,
|
||||
R001,
|
||||
R002,
|
||||
}
|
||||
|
||||
impl FromStr for CheckCode {
|
||||
@@ -29,20 +39,30 @@ impl FromStr for CheckCode {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"E402" => Ok(CheckCode::E402),
|
||||
"E501" => Ok(CheckCode::E501),
|
||||
"E711" => Ok(CheckCode::E711),
|
||||
"E712" => Ok(CheckCode::E712),
|
||||
"E713" => Ok(CheckCode::E713),
|
||||
"E714" => Ok(CheckCode::E714),
|
||||
"E731" => Ok(CheckCode::E731),
|
||||
"E902" => Ok(CheckCode::E902),
|
||||
"F401" => Ok(CheckCode::F401),
|
||||
"F403" => Ok(CheckCode::F403),
|
||||
"F541" => Ok(CheckCode::F541),
|
||||
"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),
|
||||
"F841" => Ok(CheckCode::F841),
|
||||
"F901" => Ok(CheckCode::F901),
|
||||
"R0205" => Ok(CheckCode::R0205),
|
||||
"R001" => Ok(CheckCode::R001),
|
||||
"R002" => Ok(CheckCode::R002),
|
||||
_ => Err(anyhow::anyhow!("Unknown check code: {s}")),
|
||||
}
|
||||
}
|
||||
@@ -51,40 +71,60 @@ impl FromStr for CheckCode {
|
||||
impl CheckCode {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
CheckCode::E402 => "E402",
|
||||
CheckCode::E501 => "E501",
|
||||
CheckCode::E711 => "E711",
|
||||
CheckCode::E712 => "E712",
|
||||
CheckCode::E713 => "E713",
|
||||
CheckCode::E714 => "E714",
|
||||
CheckCode::E731 => "E731",
|
||||
CheckCode::E902 => "E902",
|
||||
CheckCode::F401 => "F401",
|
||||
CheckCode::F403 => "F403",
|
||||
CheckCode::F541 => "F541",
|
||||
CheckCode::F631 => "F631",
|
||||
CheckCode::F634 => "F634",
|
||||
CheckCode::F704 => "F704",
|
||||
CheckCode::F706 => "F706",
|
||||
CheckCode::F707 => "F707",
|
||||
CheckCode::F821 => "F821",
|
||||
CheckCode::F823 => "F823",
|
||||
CheckCode::F822 => "F822",
|
||||
CheckCode::F823 => "F823",
|
||||
CheckCode::F831 => "F831",
|
||||
CheckCode::F841 => "F841",
|
||||
CheckCode::F901 => "F901",
|
||||
CheckCode::R0205 => "R0205",
|
||||
CheckCode::R001 => "R001",
|
||||
CheckCode::R002 => "R002",
|
||||
}
|
||||
}
|
||||
|
||||
/// The source for the check (either the AST, or the physical lines).
|
||||
pub fn lint_source(&self) -> &'static LintSource {
|
||||
match self {
|
||||
CheckCode::E402 => &LintSource::AST,
|
||||
CheckCode::E501 => &LintSource::Lines,
|
||||
CheckCode::E711 => &LintSource::AST,
|
||||
CheckCode::E712 => &LintSource::AST,
|
||||
CheckCode::E713 => &LintSource::AST,
|
||||
CheckCode::E714 => &LintSource::AST,
|
||||
CheckCode::E731 => &LintSource::AST,
|
||||
CheckCode::E902 => &LintSource::FileSystem,
|
||||
CheckCode::F401 => &LintSource::AST,
|
||||
CheckCode::F403 => &LintSource::AST,
|
||||
CheckCode::F541 => &LintSource::AST,
|
||||
CheckCode::F631 => &LintSource::AST,
|
||||
CheckCode::F634 => &LintSource::AST,
|
||||
CheckCode::F704 => &LintSource::AST,
|
||||
CheckCode::F706 => &LintSource::AST,
|
||||
CheckCode::F707 => &LintSource::AST,
|
||||
CheckCode::F821 => &LintSource::AST,
|
||||
CheckCode::F822 => &LintSource::AST,
|
||||
CheckCode::F823 => &LintSource::AST,
|
||||
CheckCode::F831 => &LintSource::AST,
|
||||
CheckCode::F841 => &LintSource::AST,
|
||||
CheckCode::F901 => &LintSource::AST,
|
||||
CheckCode::R0205 => &LintSource::AST,
|
||||
CheckCode::R001 => &LintSource::AST,
|
||||
CheckCode::R002 => &LintSource::AST,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,19 +133,36 @@ impl CheckCode {
|
||||
pub enum LintSource {
|
||||
AST,
|
||||
Lines,
|
||||
FileSystem,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RejectedCmpop {
|
||||
Eq,
|
||||
NotEq,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CheckKind {
|
||||
AssertTuple,
|
||||
DefaultExceptNotLast,
|
||||
DoNotAssignLambda,
|
||||
DuplicateArgumentName,
|
||||
FStringMissingPlaceholders,
|
||||
IOError(String),
|
||||
IfTuple,
|
||||
ImportStarUsage,
|
||||
LineTooLong,
|
||||
ModuleImportNotAtTopOfFile,
|
||||
NoAssertEquals,
|
||||
NoneComparison(RejectedCmpop),
|
||||
NotInTest,
|
||||
NotIsTest,
|
||||
RaiseNotImplemented,
|
||||
ReturnOutsideFunction,
|
||||
UndefinedLocal(String),
|
||||
TrueFalseComparison(bool, RejectedCmpop),
|
||||
UndefinedExport(String),
|
||||
UndefinedLocal(String),
|
||||
UndefinedName(String),
|
||||
UnusedImport(String),
|
||||
UnusedVariable(String),
|
||||
@@ -117,15 +174,25 @@ impl CheckKind {
|
||||
/// The name of the check.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
CheckKind::AssertTuple => "AssertTuple",
|
||||
CheckKind::DefaultExceptNotLast => "DefaultExceptNotLast",
|
||||
CheckKind::DuplicateArgumentName => "DuplicateArgumentName",
|
||||
CheckKind::FStringMissingPlaceholders => "FStringMissingPlaceholders",
|
||||
CheckKind::IOError(_) => "IOError",
|
||||
CheckKind::IfTuple => "IfTuple",
|
||||
CheckKind::ImportStarUsage => "ImportStarUsage",
|
||||
CheckKind::LineTooLong => "LineTooLong",
|
||||
CheckKind::DoNotAssignLambda => "DoNotAssignLambda",
|
||||
CheckKind::ModuleImportNotAtTopOfFile => "ModuleImportNotAtTopOfFile",
|
||||
CheckKind::NoAssertEquals => "NoAssertEquals",
|
||||
CheckKind::NoneComparison(_) => "NoneComparison",
|
||||
CheckKind::NotInTest => "NotInTest",
|
||||
CheckKind::NotIsTest => "NotIsTest",
|
||||
CheckKind::RaiseNotImplemented => "RaiseNotImplemented",
|
||||
CheckKind::ReturnOutsideFunction => "ReturnOutsideFunction",
|
||||
CheckKind::UndefinedLocal(_) => "UndefinedLocal",
|
||||
CheckKind::TrueFalseComparison(_, _) => "TrueFalseComparison",
|
||||
CheckKind::UndefinedExport(_) => "UndefinedExport",
|
||||
CheckKind::UndefinedLocal(_) => "UndefinedLocal",
|
||||
CheckKind::UndefinedName(_) => "UndefinedName",
|
||||
CheckKind::UnusedImport(_) => "UnusedImport",
|
||||
CheckKind::UnusedVariable(_) => "UnusedVariable",
|
||||
@@ -137,19 +204,29 @@ impl CheckKind {
|
||||
/// A four-letter shorthand code for the check.
|
||||
pub fn code(&self) -> &'static CheckCode {
|
||||
match self {
|
||||
CheckKind::AssertTuple => &CheckCode::F631,
|
||||
CheckKind::DefaultExceptNotLast => &CheckCode::F707,
|
||||
CheckKind::DuplicateArgumentName => &CheckCode::F831,
|
||||
CheckKind::FStringMissingPlaceholders => &CheckCode::F541,
|
||||
CheckKind::IOError(_) => &CheckCode::E902,
|
||||
CheckKind::IfTuple => &CheckCode::F634,
|
||||
CheckKind::ImportStarUsage => &CheckCode::F403,
|
||||
CheckKind::LineTooLong => &CheckCode::E501,
|
||||
CheckKind::DoNotAssignLambda => &CheckCode::E731,
|
||||
CheckKind::ModuleImportNotAtTopOfFile => &CheckCode::E402,
|
||||
CheckKind::NoAssertEquals => &CheckCode::R002,
|
||||
CheckKind::NoneComparison(_) => &CheckCode::E711,
|
||||
CheckKind::NotInTest => &CheckCode::E713,
|
||||
CheckKind::NotIsTest => &CheckCode::E714,
|
||||
CheckKind::RaiseNotImplemented => &CheckCode::F901,
|
||||
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
|
||||
CheckKind::TrueFalseComparison(_, _) => &CheckCode::E712,
|
||||
CheckKind::UndefinedExport(_) => &CheckCode::F822,
|
||||
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
|
||||
CheckKind::UndefinedName(_) => &CheckCode::F821,
|
||||
CheckKind::UnusedImport(_) => &CheckCode::F401,
|
||||
CheckKind::UnusedVariable(_) => &CheckCode::F841,
|
||||
CheckKind::UselessObjectInheritance(_) => &CheckCode::R0205,
|
||||
CheckKind::UselessObjectInheritance(_) => &CheckCode::R001,
|
||||
CheckKind::YieldOutsideFunction => &CheckCode::F704,
|
||||
}
|
||||
}
|
||||
@@ -157,23 +234,67 @@ impl CheckKind {
|
||||
/// The body text for the check.
|
||||
pub fn body(&self) -> String {
|
||||
match self {
|
||||
CheckKind::AssertTuple => {
|
||||
"Assert test is a non-empty tuple, which is always `True`".to_string()
|
||||
}
|
||||
CheckKind::DefaultExceptNotLast => {
|
||||
"an `except:` block as not the last exception handler".to_string()
|
||||
}
|
||||
CheckKind::DuplicateArgumentName => {
|
||||
"Duplicate argument name in function definition".to_string()
|
||||
}
|
||||
CheckKind::FStringMissingPlaceholders => {
|
||||
"f-string without any placeholders".to_string()
|
||||
}
|
||||
CheckKind::IOError(name) => {
|
||||
format!("No such file or directory: `{name}`")
|
||||
}
|
||||
CheckKind::IfTuple => "If test is a tuple, which is always `True`".to_string(),
|
||||
CheckKind::ImportStarUsage => "Unable to detect undefined names".to_string(),
|
||||
CheckKind::LineTooLong => "Line too long".to_string(),
|
||||
CheckKind::DoNotAssignLambda => {
|
||||
"Do not assign a lambda expression, use a def".to_string()
|
||||
}
|
||||
CheckKind::ModuleImportNotAtTopOfFile => {
|
||||
"Module level import not at top of file".to_string()
|
||||
}
|
||||
CheckKind::NoAssertEquals => {
|
||||
"`assertEquals` is deprecated, use `assertEqual` instead".to_string()
|
||||
}
|
||||
CheckKind::NoneComparison(op) => match op {
|
||||
RejectedCmpop::Eq => "Comparison to `None` should be `cond is None`".to_string(),
|
||||
RejectedCmpop::NotEq => {
|
||||
"Comparison to `None` should be `cond is not None`".to_string()
|
||||
}
|
||||
},
|
||||
CheckKind::NotInTest => "Test for membership should be `not in`".to_string(),
|
||||
CheckKind::NotIsTest => "Test for object identity should be `is not`".to_string(),
|
||||
CheckKind::RaiseNotImplemented => {
|
||||
"'raise NotImplemented' should be 'raise NotImplementedError".to_string()
|
||||
"`raise NotImplemented` should be `raise NotImplementedError`".to_string()
|
||||
}
|
||||
CheckKind::ReturnOutsideFunction => {
|
||||
"a `return` statement outside of a function/method".to_string()
|
||||
}
|
||||
CheckKind::TrueFalseComparison(value, op) => match *value {
|
||||
true => match op {
|
||||
RejectedCmpop::Eq => {
|
||||
"Comparison to `True` should be `cond is True`".to_string()
|
||||
}
|
||||
RejectedCmpop::NotEq => {
|
||||
"Comparison to `True` should be `cond is not True`".to_string()
|
||||
}
|
||||
},
|
||||
false => match op {
|
||||
RejectedCmpop::Eq => {
|
||||
"Comparison to `False` should be `cond is False`".to_string()
|
||||
}
|
||||
RejectedCmpop::NotEq => {
|
||||
"Comparison to `False` should be `cond is not False`".to_string()
|
||||
}
|
||||
},
|
||||
},
|
||||
CheckKind::UndefinedExport(name) => {
|
||||
format!("Undefined name `{name}` in __all__")
|
||||
format!("Undefined name `{name}` in `__all__`")
|
||||
}
|
||||
CheckKind::UndefinedName(name) => {
|
||||
format!("Undefined name `{name}`")
|
||||
@@ -186,19 +307,58 @@ impl CheckKind {
|
||||
format!("Local variable `{name}` is assigned to but never used")
|
||||
}
|
||||
CheckKind::UselessObjectInheritance(name) => {
|
||||
format!("Class {name} inherits from object")
|
||||
format!("Class `{name}` inherits from object")
|
||||
}
|
||||
CheckKind::YieldOutsideFunction => {
|
||||
"a `yield` or `yield from` statement outside of a function/method".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the check kind is (potentially) fixable.
|
||||
pub fn fixable(&self) -> bool {
|
||||
match self {
|
||||
CheckKind::AssertTuple => false,
|
||||
CheckKind::DefaultExceptNotLast => false,
|
||||
CheckKind::DuplicateArgumentName => false,
|
||||
CheckKind::FStringMissingPlaceholders => false,
|
||||
CheckKind::IOError(_) => false,
|
||||
CheckKind::IfTuple => false,
|
||||
CheckKind::ImportStarUsage => false,
|
||||
CheckKind::DoNotAssignLambda => false,
|
||||
CheckKind::LineTooLong => false,
|
||||
CheckKind::ModuleImportNotAtTopOfFile => false,
|
||||
CheckKind::NoAssertEquals => true,
|
||||
CheckKind::NotInTest => false,
|
||||
CheckKind::NotIsTest => false,
|
||||
CheckKind::NoneComparison(_) => false,
|
||||
CheckKind::RaiseNotImplemented => false,
|
||||
CheckKind::ReturnOutsideFunction => false,
|
||||
CheckKind::TrueFalseComparison(_, _) => 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(|| {
|
||||
@@ -207,6 +367,18 @@ static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
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 {
|
||||
match NO_QA_REGEX.captures(line) {
|
||||
Some(caps) => match caps.name("codes") {
|
||||
|
||||
126
src/fixer.rs
Normal file
126
src/fixer.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
extern crate core;
|
||||
|
||||
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;
|
||||
|
||||
684
src/linter.rs
684
src/linter.rs
@@ -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,10 +84,33 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::checks::{CheckCode, CheckKind};
|
||||
use crate::checks::{Check, CheckCode, CheckKind, Fix, RejectedCmpop};
|
||||
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<()> {
|
||||
@@ -74,12 +121,12 @@ mod tests {
|
||||
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/fixtures/E501.py".to_string(),
|
||||
fix: None,
|
||||
}];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
for i in 0..actual.len() {
|
||||
@@ -89,6 +136,152 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e711() -> Result<()> {
|
||||
let actual = check_path(
|
||||
Path::new("./resources/test/fixtures/E711.py"),
|
||||
&settings::Settings {
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E711]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![
|
||||
Check {
|
||||
kind: CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
location: Location::new(1, 11),
|
||||
fix: None,
|
||||
},
|
||||
Check {
|
||||
kind: CheckKind::NoneComparison(RejectedCmpop::NotEq),
|
||||
location: Location::new(4, 4),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
for i in 0..actual.len() {
|
||||
assert_eq!(actual[i], expected[i]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e712() -> Result<()> {
|
||||
let actual = check_path(
|
||||
Path::new("./resources/test/fixtures/E712.py"),
|
||||
&settings::Settings {
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E712]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![
|
||||
Check {
|
||||
kind: CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
|
||||
location: Location::new(1, 11),
|
||||
fix: None,
|
||||
},
|
||||
Check {
|
||||
kind: CheckKind::TrueFalseComparison(false, RejectedCmpop::NotEq),
|
||||
location: Location::new(4, 4),
|
||||
fix: None,
|
||||
},
|
||||
Check {
|
||||
kind: CheckKind::TrueFalseComparison(false, RejectedCmpop::NotEq),
|
||||
location: Location::new(7, 11),
|
||||
fix: None,
|
||||
},
|
||||
Check {
|
||||
kind: CheckKind::TrueFalseComparison(true, RejectedCmpop::NotEq),
|
||||
location: Location::new(7, 20),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
for i in 0..actual.len() {
|
||||
assert_eq!(actual[i], expected[i]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e713() -> Result<()> {
|
||||
let actual = check_path(
|
||||
Path::new("./resources/test/fixtures/E713.py"),
|
||||
&settings::Settings {
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E713]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![Check {
|
||||
kind: CheckKind::NotInTest,
|
||||
location: Location::new(2, 12),
|
||||
fix: None,
|
||||
}];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
for i in 0..actual.len() {
|
||||
assert_eq!(actual[i], expected[i]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e714() -> Result<()> {
|
||||
let actual = check_path(
|
||||
Path::new("./resources/test/fixtures/E714.py"),
|
||||
&settings::Settings {
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E714]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![Check {
|
||||
kind: CheckKind::NotIsTest,
|
||||
location: Location::new(1, 13),
|
||||
fix: None,
|
||||
}];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
for i in 0..actual.len() {
|
||||
assert_eq!(actual[i], expected[i]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e731() -> Result<()> {
|
||||
let actual = check_path(
|
||||
Path::new("./resources/test/fixtures/E731.py"),
|
||||
&settings::Settings {
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::E731]),
|
||||
},
|
||||
&autofix::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![Check {
|
||||
kind: CheckKind::DoNotAssignLambda,
|
||||
location: Location::new(1, 1),
|
||||
fix: None,
|
||||
}];
|
||||
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
for i in 0..actual.len() {
|
||||
assert_eq!(actual[i], expected[i]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f401() -> Result<()> {
|
||||
let actual = check_path(
|
||||
@@ -98,23 +291,23 @@ mod tests {
|
||||
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(12, 1),
|
||||
filename: "./resources/test/fixtures/F401.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::UnusedImport("functools".to_string()),
|
||||
location: Location::new(3, 1),
|
||||
filename: "./resources/test/fixtures/F401.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::UnusedImport("collections.OrderedDict".to_string()),
|
||||
location: Location::new(4, 1),
|
||||
filename: "./resources/test/fixtures/F401.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
@@ -134,18 +327,18 @@ mod tests {
|
||||
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/fixtures/F403.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::ImportStarUsage,
|
||||
location: Location::new(2, 1),
|
||||
filename: "./resources/test/fixtures/F403.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
@@ -164,23 +357,54 @@ mod tests {
|
||||
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/fixtures/F541.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::FStringMissingPlaceholders,
|
||||
location: Location::new(5, 7),
|
||||
filename: "./resources/test/fixtures/F541.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::FStringMissingPlaceholders,
|
||||
location: Location::new(7, 7),
|
||||
filename: "./resources/test/fixtures/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());
|
||||
@@ -200,18 +424,18 @@ mod tests {
|
||||
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/fixtures/F634.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::IfTuple,
|
||||
location: Location::new(7, 5),
|
||||
filename: "./resources/test/fixtures/F634.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
@@ -231,23 +455,23 @@ mod tests {
|
||||
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/fixtures/F704.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::YieldOutsideFunction,
|
||||
location: Location::new(9, 1),
|
||||
filename: "./resources/test/fixtures/F704.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::YieldOutsideFunction,
|
||||
location: Location::new(10, 1),
|
||||
filename: "./resources/test/fixtures/F704.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
@@ -267,18 +491,54 @@ mod tests {
|
||||
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/fixtures/F706.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::ReturnOutsideFunction,
|
||||
location: Location::new(9, 1),
|
||||
filename: "./resources/test/fixtures/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());
|
||||
@@ -298,28 +558,28 @@ mod tests {
|
||||
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/fixtures/F821.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::UndefinedName("self".to_string()),
|
||||
location: Location::new(6, 13),
|
||||
filename: "./resources/test/fixtures/F821.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::UndefinedName("self".to_string()),
|
||||
location: Location::new(10, 9),
|
||||
filename: "./resources/test/fixtures/F821.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::UndefinedName("numeric_string".to_string()),
|
||||
location: Location::new(21, 12),
|
||||
filename: "./resources/test/fixtures/F821.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
@@ -339,12 +599,12 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F822]),
|
||||
},
|
||||
&cache::Mode::None,
|
||||
&autofix::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![Message {
|
||||
let expected = vec![Check {
|
||||
kind: CheckKind::UndefinedExport("b".to_string()),
|
||||
location: Location::new(3, 1),
|
||||
filename: "./resources/test/fixtures/F822.py".to_string(),
|
||||
fix: None,
|
||||
}];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
for i in 0..actual.len() {
|
||||
@@ -363,12 +623,12 @@ mod tests {
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::F823]),
|
||||
},
|
||||
&cache::Mode::None,
|
||||
&autofix::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![Message {
|
||||
let expected = vec![Check {
|
||||
kind: CheckKind::UndefinedLocal("my_var".to_string()),
|
||||
location: Location::new(6, 5),
|
||||
filename: "./resources/test/fixtures/F823.py".to_string(),
|
||||
fix: None,
|
||||
}];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
for i in 0..actual.len() {
|
||||
@@ -387,23 +647,23 @@ mod tests {
|
||||
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/fixtures/F831.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::DuplicateArgumentName,
|
||||
location: Location::new(5, 28),
|
||||
filename: "./resources/test/fixtures/F831.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::DuplicateArgumentName,
|
||||
location: Location::new(9, 27),
|
||||
filename: "./resources/test/fixtures/F831.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
@@ -423,18 +683,18 @@ mod tests {
|
||||
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/fixtures/F841.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::UnusedVariable("z".to_string()),
|
||||
location: Location::new(16, 5),
|
||||
filename: "./resources/test/fixtures/F841.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
@@ -454,18 +714,18 @@ mod tests {
|
||||
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/fixtures/F901.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
Message {
|
||||
Check {
|
||||
kind: CheckKind::RaiseNotImplemented,
|
||||
location: Location::new(6, 5),
|
||||
filename: "./resources/test/fixtures/F901.py".to_string(),
|
||||
fix: None,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
@@ -477,31 +737,257 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn r0205() -> Result<()> {
|
||||
fn r001() -> Result<()> {
|
||||
let actual = check_path(
|
||||
Path::new("./resources/test/fixtures/R0205.py"),
|
||||
Path::new("./resources/test/fixtures/R001.py"),
|
||||
&settings::Settings {
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
select: BTreeSet::from([CheckCode::R0205]),
|
||||
select: BTreeSet::from([CheckCode::R001]),
|
||||
},
|
||||
&cache::Mode::None,
|
||||
&autofix::Mode::Generate,
|
||||
)?;
|
||||
let expected = vec![
|
||||
Message {
|
||||
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(5, 1),
|
||||
filename: "./resources/test/fixtures/R0205.py".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,
|
||||
}),
|
||||
},
|
||||
Message {
|
||||
kind: CheckKind::UselessObjectInheritance("C".to_string()),
|
||||
location: Location::new(9, 1),
|
||||
filename: "./resources/test/fixtures/R0205.py".to_string(),
|
||||
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,
|
||||
}),
|
||||
},
|
||||
Message {
|
||||
kind: CheckKind::UselessObjectInheritance("D".to_string()),
|
||||
location: Location::new(14, 5),
|
||||
filename: "./resources/test/fixtures/R0205.py".to_string(),
|
||||
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());
|
||||
|
||||
100
src/main.rs
100
src/main.rs
@@ -13,14 +13,18 @@ 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;
|
||||
use ruff::checks::CheckKind;
|
||||
|
||||
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 +41,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,10 +55,15 @@ 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
|
||||
let paths: Vec<DirEntry> = files
|
||||
.iter()
|
||||
.flat_map(|path| iter_python_files(path, &settings.exclude))
|
||||
.collect();
|
||||
@@ -59,16 +71,30 @@ fn run_once(files: &[PathBuf], settings: &Settings, cache: bool) -> Result<Vec<M
|
||||
debug!("Identified files to lint in: {:?}", duration);
|
||||
|
||||
let start = Instant::now();
|
||||
let mut messages: Vec<Message> = files
|
||||
let mut messages: Vec<Message> = paths
|
||||
.par_iter()
|
||||
.map(|entry| {
|
||||
check_path(entry.path(), settings, &cache.into()).unwrap_or_else(|e| {
|
||||
lint_path(entry.path(), settings, &cache.into(), &autofix.into()).unwrap_or_else(|e| {
|
||||
error!("Failed to check {}: {e:?}", entry.path().to_string_lossy());
|
||||
vec![]
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
if settings.select.contains(&CheckCode::E902) {
|
||||
for file in files {
|
||||
if !file.exists() {
|
||||
messages.push(Message {
|
||||
kind: CheckKind::IOError(file.to_string_lossy().to_string()),
|
||||
fixed: false,
|
||||
location: Default::default(),
|
||||
filename: file.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages.sort_unstable();
|
||||
let duration = start.elapsed();
|
||||
debug!("Checked files in: {:?}", duration);
|
||||
@@ -77,13 +103,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 +166,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 +194,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 +205,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 +220,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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -237,20 +237,30 @@ other-attribute = 1
|
||||
Path::new("**/migrations").to_path_buf()
|
||||
]),
|
||||
select: Some(BTreeSet::from([
|
||||
CheckCode::E402,
|
||||
CheckCode::E501,
|
||||
CheckCode::E711,
|
||||
CheckCode::E712,
|
||||
CheckCode::E713,
|
||||
CheckCode::E714,
|
||||
CheckCode::E731,
|
||||
CheckCode::E902,
|
||||
CheckCode::F401,
|
||||
CheckCode::F403,
|
||||
CheckCode::F541,
|
||||
CheckCode::F631,
|
||||
CheckCode::F634,
|
||||
CheckCode::F704,
|
||||
CheckCode::F706,
|
||||
CheckCode::F707,
|
||||
CheckCode::F821,
|
||||
CheckCode::F822,
|
||||
CheckCode::F823,
|
||||
CheckCode::F831,
|
||||
CheckCode::F841,
|
||||
CheckCode::F901,
|
||||
CheckCode::R0205,
|
||||
CheckCode::R001,
|
||||
CheckCode::R002,
|
||||
])),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -44,15 +44,31 @@ impl Settings {
|
||||
.collect(),
|
||||
select: config.select.unwrap_or_else(|| {
|
||||
BTreeSet::from([
|
||||
CheckCode::E402,
|
||||
CheckCode::E501,
|
||||
CheckCode::E711,
|
||||
CheckCode::E712,
|
||||
CheckCode::E713,
|
||||
CheckCode::E714,
|
||||
CheckCode::E731,
|
||||
CheckCode::E902,
|
||||
CheckCode::F401,
|
||||
CheckCode::F403,
|
||||
CheckCode::F541,
|
||||
CheckCode::F631,
|
||||
CheckCode::F634,
|
||||
CheckCode::F704,
|
||||
CheckCode::F706,
|
||||
CheckCode::F831,
|
||||
CheckCode::F707,
|
||||
CheckCode::F821,
|
||||
CheckCode::F822,
|
||||
CheckCode::F823,
|
||||
CheckCode::F831,
|
||||
CheckCode::F841,
|
||||
CheckCode::F901,
|
||||
// Disable refactoring codes by default.
|
||||
// CheckCode::R001,
|
||||
// CheckCode::R002,
|
||||
])
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user