Compare commits

..

96 Commits

Author SHA1 Message Date
Charlie Marsh
2f69be0d41 Bump version to 0.0.71 2022-10-12 17:14:28 -04:00
Charlie Marsh
54cb2eb15b Only run section checks when CheckCodes are enabled 2022-10-12 17:14:16 -04:00
Charlie Marsh
167992ad48 Implement D407, D408, D409, D412, and D414 (#413) 2022-10-12 17:12:54 -04:00
Charlie Marsh
f0dab24079 Implement D405, D406, D410, D411, and D413 (#411) 2022-10-12 16:31:14 -04:00
Charlie Marsh
77055faab6 Implement D404 and D418 for pydocstyle (#409) 2022-10-12 13:20:55 -04:00
Charlie Marsh
e08e1caf71 Bump version to 0.0.70 2022-10-12 12:59:14 -04:00
Harutaka Kawamura
0072dfd81e Implement C414 (#406) 2022-10-12 12:58:46 -04:00
Charlie Marsh
6ffe02ee05 Pass around VisibleScope 2022-10-12 12:52:48 -04:00
Charlie Marsh
688fc0cd02 Implement docstring visibility checks (#408) 2022-10-12 12:46:40 -04:00
Charlie Marsh
f30e5e45ab Remove initial field 2022-10-12 11:07:31 -04:00
Charlie Marsh
1a68a38306 Enable definition tracking for docstrings (#407) 2022-10-12 11:06:28 -04:00
Charlie Marsh
590aa92ead Implement D201, D202, D203, D204, and D211 (#404) 2022-10-11 21:08:30 -04:00
Charlie Marsh
8868f57a74 Implement D402 for pydocstyle (#403) 2022-10-11 13:19:56 -04:00
Charlie Marsh
71802f8861 Bump version to 0.0.69 2022-10-11 12:54:56 -04:00
Charlie Marsh
5b6fb8cefa Skip docstring checks for empty docstrings (#402) 2022-10-11 12:54:30 -04:00
Charlie Marsh
2ff964107c Add D212, D213, D300, D403, and D415 (#400) 2022-10-11 12:48:23 -04:00
Charlie Marsh
141132d5be Add fake setup.py (#399) 2022-10-11 12:38:48 -04:00
Harutaka Kawamura
8ba872ece4 Support linting input from stdin (#387) 2022-10-11 09:56:20 -04:00
Charlie Marsh
209dce2033 Fix missing backtick 2022-10-10 17:23:58 -04:00
Charlie Marsh
90d88dfb10 Make pyupgrade tally more precise 2022-10-10 17:18:20 -04:00
Charlie Marsh
4730911b25 Bump version to 0.0.68 2022-10-10 16:50:09 -04:00
Charlie Marsh
4e9fb9907a Implement D205, D209, and D210 (#398) 2022-10-10 16:49:51 -04:00
Charlie Marsh
b8dce8922d Implement D200 (OneLinerDocstring) (#397) 2022-10-10 16:07:51 -04:00
Charlie Marsh
30877127bc Implement D400 (DocstringEndsInNonPeriod) (#396) 2022-10-10 15:35:23 -04:00
Charlie Marsh
8b66bbdc9b Regenerate rules table 2022-10-10 15:18:18 -04:00
Charlie Marsh
71d3a84b14 Implement D410 (EmptyDocstring) (#395) 2022-10-10 15:15:38 -04:00
Charlie Marsh
323a5c857c Implement docstring tracking (#394) 2022-10-10 15:15:09 -04:00
Charlie Marsh
42cec3f5a0 Regenerate rules table 2022-10-10 14:02:31 -04:00
Charlie Marsh
ee42413e10 Enable autofix for B014 2022-10-10 13:18:43 -04:00
Charlie Marsh
765db12b84 Remove check_ prefix from check utilities (#393) 2022-10-10 12:58:40 -04:00
Charlie Marsh
e1b711d9c6 Bump version to 0.0.67 2022-10-10 12:55:04 -04:00
Charlie Marsh
35f593846e Implement B014 from flake8-bugbear (#392) 2022-10-10 12:53:42 -04:00
Charlie Marsh
c384fa513b Implement B025 from flake8-bugbear (#391) 2022-10-10 12:18:31 -04:00
Charlie Marsh
022ff64d29 Implement B011 from flake8-bugbear (#390) 2022-10-10 10:55:55 -04:00
Charlie Marsh
5a06fb28fd Bump version to 0.0.66 2022-10-10 10:03:59 -04:00
Charlie Marsh
46750a3e17 Flag unimplemented error codes in M001 (#388) 2022-10-10 10:03:40 -04:00
Charlie Marsh
9cc902b802 Avoid F821 false-positives with NameError (#386) 2022-10-10 09:39:59 -04:00
Harutaka Kawamura
c2a36ebd1e Implement C410 (#382) 2022-10-09 23:33:55 -04:00
Charlie Marsh
34ca225393 Rename flakes8 to flake8 2022-10-09 23:02:44 -04:00
Harutaka Kawamura
38c30905e6 Implement C409 (#381) 2022-10-09 22:34:52 -04:00
Charlie Marsh
2774194b03 Bump version to 0.0.65 2022-10-09 22:14:04 -04:00
Charlie Marsh
71ebd39f35 Extend assertEquals check to all deprecated unittest aliases (#380) 2022-10-09 22:13:51 -04:00
Charlie Marsh
a2f78ba2c7 Fix auto-fix for assertEquals rename 2022-10-09 22:11:32 -04:00
Charlie Marsh
b51a080a44 Rename SPR001 to U008 (#379) 2022-10-09 21:58:22 -04:00
Charlie Marsh
6a1d7d8a1c Defer string annotations even when futures annotations are enabled (#378) 2022-10-09 18:28:29 -04:00
Charlie Marsh
10b250ee57 Bump version to 0.0.64 2022-10-09 17:38:09 -04:00
Charlie Marsh
30b1b1e15a Treat TypeAlias values as annotations (#377) 2022-10-09 17:37:19 -04:00
Charlie Marsh
aafe7c0c39 Mark aliased submodule imports as used (#374) 2022-10-09 17:01:14 -04:00
Harutaka Kawamura
f060248656 Fix collapsed message (#372) 2022-10-09 13:01:36 -04:00
Harutaka Kawamura
bbe0220c72 Implement C415 (#371) 2022-10-09 10:12:58 -04:00
Charlie Marsh
129e2b6ad3 Bump version to 0.0.63 2022-10-08 22:51:49 -04:00
Charlie Marsh
73e744b1d0 Create unified Expr for PEP 604 rewrites (#370) 2022-10-08 22:13:00 -04:00
Charlie Marsh
50a3fc5a67 Move some code into helpers.rs 2022-10-08 20:45:15 -04:00
Charlie Marsh
de499f0258 Optimize imports 2022-10-08 20:39:13 -04:00
Charlie Marsh
e1abe37c6a Bump version to 0.0.62 2022-10-08 20:28:38 -04:00
Charlie Marsh
7fe5945541 Implement PEP 604 annotation rewrites (#369) 2022-10-08 20:28:00 -04:00
Charlie Marsh
806f3fd4f6 Implement PEP 585 annotation rewrites (#368) 2022-10-08 18:22:24 -04:00
Charlie Marsh
2bba643dd2 Use strum to facilitate simple enum serialization (#367) 2022-10-08 17:47:25 -04:00
Charlie Marsh
54090bd7ac Use strum to iterate over all check codes (#366) 2022-10-08 17:41:47 -04:00
Charlie Marsh
c62727db42 Bump version to 0.0.61 2022-10-08 17:25:36 -04:00
Charlie Marsh
d0e1612507 Check newline ending on contents directly (#365) 2022-10-08 17:25:22 -04:00
Harutaka Kawamura
5ccd907398 Implement C408 (#364) 2022-10-08 17:17:34 -04:00
Harutaka Kawamura
346610c2e3 Implement C406 (#363) 2022-10-08 11:15:41 -04:00
Harutaka Kawamura
307fa26515 Implement C405 (#362) 2022-10-08 09:03:21 -04:00
Harutaka Kawamura
136d412edd Add missing C400,C401, and C402 to CheckCode.from_str (#361) 2022-10-08 09:02:03 -04:00
Harutaka Kawamura
d9edec0ac9 Implement C402 (#359) 2022-10-07 22:57:04 -04:00
Chris Pryer
473675fffb Add check for W292 (#339) 2022-10-07 21:10:16 -04:00
Steven Maude
95dfc61315 Update GitHub Actions versions in README (#358) 2022-10-07 20:24:00 -04:00
Charlie Marsh
dd496c7b52 Bump version to 0.0.60 2022-10-07 17:36:33 -04:00
Charlie Marsh
78aafb4b34 Warn the user if an explicitly selected check code is ignored (#356) 2022-10-07 17:36:17 -04:00
Charlie Marsh
99c66d513a Rename refactor checks to upgrade checks (#354) 2022-10-07 16:54:55 -04:00
Charlie Marsh
04ade6a2f3 Update README.md 2022-10-07 16:50:17 -04:00
Charlie Marsh
4cf2682cda Implement type(primitive) (#353) 2022-10-07 16:47:06 -04:00
Charlie Marsh
e3a7357187 Wrap each import in its own backticks (#346) 2022-10-07 15:58:30 -04:00
Charlie Marsh
b60768debb Remove erroneous output.py file 2022-10-07 15:04:23 -04:00
Charlie Marsh
25e476639f Use or_default in lieu of or_insert 2022-10-07 14:56:56 -04:00
Charlie Marsh
4645788205 Bump version to 0.0.59 2022-10-07 14:55:23 -04:00
Charlie Marsh
0b9eda8836 Add target Python version as a configurable setting (#344) 2022-10-07 14:54:50 -04:00
Charlie Marsh
ad23d6acee Remove :: prefix for ruff imports 2022-10-07 14:24:51 -04:00
Harutaka Kawamura
e3d1d01a1f Implement C401 (#343) 2022-10-07 13:00:22 -04:00
Harutaka Kawamura
6dfdd21a7c Implement C400 (#340) 2022-10-07 12:16:23 -04:00
Charlie Marsh
f17d3b3c44 Bump version to 0.0.58 2022-10-07 12:14:03 -04:00
Charlie Marsh
da6b913317 Exit 0 if all errors are fixed (#342) 2022-10-07 12:13:15 -04:00
Harutaka Kawamura
bd34850f98 Implement C404 (#338) 2022-10-07 08:53:18 -04:00
Charlie Marsh
d5b33cdb40 Add missing snapshot for U002 2022-10-07 08:48:59 -04:00
Charlie Marsh
3fb4cf7009 Hide autoformat argument 2022-10-06 22:56:01 -04:00
Charlie Marsh
6e19539e28 Enable abspath(__file__) removal (#336) 2022-10-06 22:49:06 -04:00
Charlie Marsh
4eac7a07f5 Mention flake8-comprehensions in README 2022-10-06 16:26:21 -04:00
Harutaka Kawamura
5141285c8e Implement C403 (#335) 2022-10-06 16:24:23 -04:00
Charlie Marsh
82cc139d2d Bump version to 0.0.57 2022-10-06 09:16:56 -04:00
Adrian Garcia Badaracco
df438ba051 Support PEP 593 annotations (#333) 2022-10-06 09:16:07 -04:00
Adrian Garcia Badaracco
a70624cd47 add instructions for setting up cargo insta (#334) 2022-10-06 08:01:11 -04:00
Charlie Marsh
b307afc00c Move some accesses behind a shared function 2022-10-05 14:54:31 -04:00
Charlie Marsh
3d5bc1f51f Migrate Checker logic to independent plugins (#331) 2022-10-05 14:08:40 -04:00
Charlie Marsh
aba01745f5 Bump version to 0.0.56 2022-10-05 11:58:54 -04:00
Charlie Marsh
1eeeffab66 Add T201 and T203 to string conversion match (#332) 2022-10-05 11:58:33 -04:00
158 changed files with 9069 additions and 1189 deletions

View File

@@ -14,6 +14,12 @@ your proposed change.
ruff is written in Rust (1.63.0). You'll need to install the
[Rust toolchain](https://www.rust-lang.org/tools/install) for development.
You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
```shell
cargo install cargo-insta
```
### Development
After cloning the repository, run ruff locally with:
@@ -58,7 +64,7 @@ similar rules are implemented.
To add a test fixture, create a file under `resources/test/fixtures`, named to match the `CheckCode`
you defined earlier (e.g., `E402.py`). This file should contain a variety of violations and
non-violations designed to evaluate and demonstrate the behavior of your lint rule. Run ruff locally
with (e.g.) `cargo run resources/test/fixtures/E402.py`. Once you're satisified with the output,
with (e.g.) `cargo run resources/test/fixtures/E402.py`. Once you're satisfied with the output,
codify the behavior as a snapshot test by adding a new function to the `mod tests` section of
`src/linter.rs`, like so:

122
Cargo.lock generated
View File

@@ -64,6 +64,20 @@ dependencies = [
"term",
]
[[package]]
name = "assert_cmd"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e"
dependencies = [
"bstr",
"doc-comment",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "async-channel"
version = "1.7.1"
@@ -580,6 +594,12 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.8.1"
@@ -658,6 +678,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "either"
version = "1.8.0"
@@ -1089,6 +1115,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
[[package]]
name = "joinery"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5"
[[package]]
name = "js-sys"
version = "0.3.60"
@@ -1685,6 +1717,33 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c"
dependencies = [
"difflib",
"itertools",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb"
[[package]]
name = "predicates-tree"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -1907,9 +1966,10 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.55"
version = "0.0.71"
dependencies = [
"anyhow",
"assert_cmd",
"bincode",
"cacache",
"chrono",
@@ -1926,6 +1986,7 @@ dependencies = [
"libcst",
"log",
"notify",
"num-bigint",
"once_cell",
"path-absolutize",
"rayon",
@@ -1935,6 +1996,9 @@ dependencies = [
"rustpython-parser",
"serde",
"serde_json",
"strum",
"strum_macros",
"titlecase",
"toml",
"update-informer",
"walkdir",
@@ -1955,7 +2019,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -1965,7 +2029,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
dependencies = [
"ascii",
"cfg-if 1.0.0",
@@ -1988,7 +2052,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
dependencies = [
"bincode",
"bitflags",
@@ -2005,7 +2069,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
dependencies = [
"ahash",
"anyhow",
@@ -2252,6 +2316,28 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "1.0.101"
@@ -2320,6 +2406,12 @@ dependencies = [
"phf_codegen 0.8.0",
]
[[package]]
name = "termtree"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
[[package]]
name = "thiserror"
version = "1.0.37"
@@ -2375,6 +2467,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "titlecase"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38397a8cdb017cfeb48bf6c154d6de975ac69ffeed35980fde199d2ee0842042"
dependencies = [
"joinery",
"lazy_static",
"regex",
]
[[package]]
name = "toml"
version = "0.5.9"
@@ -2568,6 +2671,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8e76fae08f03f96e166d2dfda232190638c10e0383841252416f9cfe2ae60e6"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.55"
version = "0.0.71"
edition = "2021"
[lib]
@@ -27,16 +27,21 @@ once_cell = { version = "1.13.1" }
path-absolutize = { version = "3.0.13", features = ["once_cell_cache"] }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/charliermarsh/RustPython.git", rev = "4f457893efc381ad5c432576b24bcc7e4a08c641" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "4f457893efc381ad5c432576b24bcc7e4a08c641" }
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "4f457893efc381ad5c432576b24bcc7e4a08c641" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" }
toml = { version = "0.5.9" }
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true }
walkdir = { version = "2.3.2" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = "0.24.3"
num-bigint = "0.4.3"
titlecase = "2.2.1"
[dev-dependencies]
assert_cmd = "2.0.4"
insta = { version = "1.19.1", features = ["yaml"] }
[features]

View File

@@ -57,7 +57,7 @@ ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.48
rev: v0.0.71
hooks:
- id: lint
```
@@ -130,6 +130,8 @@ Options:
Enable automatic additions of noqa directives to failing lines
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
-h, --help
Print help information
-V, --version
@@ -213,7 +215,10 @@ ruff also implements some of the most popular Flake8 plugins natively, including
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (partial)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (12/16)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (37/48)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
@@ -243,6 +248,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | ✅ | |
| E902 | IOError | IOError: `...` | ✅ | |
| E999 | SyntaxError | SyntaxError: `...` | ✅ | |
| W292 | NoNewLineAtEndOfFile | No newline at end of file | ✅ | |
| F401 | UnusedImport | `...` imported but unused | ✅ | 🛠 |
| F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable | ✅ | |
| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names | ✅ | |
@@ -274,12 +280,68 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin | | |
| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin | | |
| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin | | |
| SPR001 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | | 🛠 |
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | | 🛠 |
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | | 🛠 |
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | | |
| C400 | UnnecessaryGeneratorList | Unnecessary generator - rewrite as a list comprehension | | |
| C401 | UnnecessaryGeneratorSet | Unnecessary generator - rewrite as a set comprehension | | |
| C402 | UnnecessaryGeneratorDict | Unnecessary generator - rewrite as a dict comprehension | | |
| C403 | UnnecessaryListComprehensionSet | Unnecessary list comprehension - rewrite as a set comprehension | | |
| C404 | UnnecessaryListComprehensionDict | Unnecessary list comprehension - rewrite as a dict comprehension | | |
| C405 | UnnecessaryLiteralSet | Unnecessary <list/tuple> literal - rewrite as a set literal | | |
| C406 | UnnecessaryLiteralDict | Unnecessary <list/tuple> literal - rewrite as a dict literal | | |
| C408 | UnnecessaryCollectionCall | Unnecessary <dict/list/tuple> call - rewrite as a literal | | |
| C409 | UnnecessaryLiteralWithinTupleCall | Unnecessary <list/tuple> literal passed to tuple() - remove the outer call to tuple() | | |
| C410 | UnnecessaryLiteralWithinListCall | Unnecessary <list/tuple> literal passed to list() - rewrite as a list literal | | |
| C414 | UnnecessaryDoubleCastOrProcess | Unnecessary <list/reversed/set/sorted/tuple> call within <list/set/sorted/tuple>(). | | |
| C415 | UnnecessarySubscriptReversal | Unnecessary subscript reversal of iterable within <reversed/set/sorted>() | | |
| T201 | PrintFound | `print` found | | 🛠 |
| T203 | PPrintFound | `pprint` found | | 🛠 |
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | | 🛠 |
| R001 | UselessObjectInheritance | Class `...` inherits from object | | 🛠 |
| R002 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 |
| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | | 🛠 |
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | | 🛠 |
| U004 | UselessObjectInheritance | Class `...` inherits from object | | 🛠 |
| U005 | DeprecatedUnittestAlias | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 |
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | | 🛠 |
| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | | 🛠 |
| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | | 🛠 |
| D100 | PublicModule | Missing docstring in public module | | |
| D101 | PublicClass | Missing docstring in public class | | |
| D102 | PublicMethod | Missing docstring in public method | | |
| D103 | PublicFunction | Missing docstring in public function | | |
| D104 | PublicPackage | Missing docstring in public package | | |
| D105 | MagicMethod | Missing docstring in magic method | | |
| D106 | PublicNestedClass | Missing docstring in public nested class | | |
| D107 | PublicInit | Missing docstring in __init__ | | |
| D200 | FitsOnOneLine | One-line docstring should fit on one line | | |
| D205 | NoBlankLineAfterSummary | 1 blank line required between summary line and description | | |
| D209 | NewLineAfterLastParagraph | Multi-line docstring closing quotes should be on a separate line | | |
| D210 | NoSurroundingWhitespace | No whitespaces allowed surrounding docstring text | | |
| D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line | | |
| D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | | |
| D300 | UsesTripleQuotes | Use """triple double quotes""" | | |
| D400 | EndsInPeriod | First line should end with a period | | |
| D402 | NoSignature | First line should not be the function's 'signature' | | |
| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | | |
| D404 | NoThisPrefix | First word of the docstring should not be `This` | | |
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | |
| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | | |
| D419 | NonEmpty | Docstring is empty | | |
| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | | |
| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | | |
| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | | |
| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring | | |
| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | | |
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | | |
| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | | |
| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | | |
| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | | |
| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | | |
| D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | | |
| D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") | | |
| D409 | SectionUnderlineMatchesSectionLength | Section underline should match the length of its name ("Returns") | | |
| D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") | | |
| D414 | NonEmptySection | Section has no content ("Returns") | | |
| M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 |
## Integrations
@@ -307,9 +369,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install dependencies

View File

@@ -1,13 +1,12 @@
/// Generate a Markdown-compatible table of supported lint rules.
use ruff::checks::{CheckCode, ALL_CHECK_CODES, DEFAULT_CHECK_CODES};
use strum::IntoEnumIterator;
use ruff::checks::{CheckCode, DEFAULT_CHECK_CODES};
fn main() {
let mut check_codes: Vec<CheckCode> = ALL_CHECK_CODES.to_vec();
check_codes.sort();
println!("| Code | Name | Message | | |");
println!("| ---- | ---- | ------- | --- | --- |");
for check_code in check_codes {
for check_code in CheckCode::iter() {
let check_kind = check_code.kind();
let default_token = if DEFAULT_CHECK_CODES.contains(&check_code) {
""
@@ -17,9 +16,9 @@ fn main() {
let fix_token = if check_kind.fixable() { "🛠" } else { "" };
println!(
"| {} | {} | {} | {} | {} |",
check_kind.code().as_str(),
check_kind.name(),
check_kind.body(),
check_kind.code().as_ref(),
check_kind.as_ref(),
check_kind.body().replace("|", r"\|"),
default_token,
fix_token
);

View File

@@ -1,37 +0,0 @@
from collections import Counter
def f() -> None:
"""Docstring goes here."""
for x in range(5):
print(x)
else:
print("Nope!")
a = {
"a",
"b",
"c",
"a",
"b",
"c",
"a",
"b",
"c",
"a",
"b",
"c",
"a",
"b",
"c",
"a",
"b",
"c",
"a",
"b",
"c",
}
cls(title=title, before_text=before_text, after_text=after_text, before_description=before_description, after_description=after_description, transform_type=transform_type)

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

@@ -0,0 +1,10 @@
"""
Should emit:
B011 - on line 8
B011 - on line 10
"""
assert 1 != 2
assert False
assert 1 != 2, "message"
assert False, "message"

76
resources/test/fixtures/B014.py vendored Normal file
View File

@@ -0,0 +1,76 @@
"""
Should emit:
B014 - on lines 11, 17, 28, 42, 49, 56, and 74.
"""
import binascii
import re
try:
pass
except (Exception, TypeError):
# TypeError is a subclass of Exception, so it doesn't add anything
pass
try:
pass
except (OSError, OSError) as err:
# Duplicate exception types are useless
pass
class MyError(Exception):
pass
try:
pass
except (MyError, MyError):
# Detect duplicate non-builtin errors
pass
try:
pass
except (MyError, Exception) as e:
# Don't assume that we're all subclasses of Exception
pass
try:
pass
except (MyError, BaseException) as e:
# But we *can* assume that everything is a subclass of BaseException
pass
try:
pass
except (re.error, re.error):
# Duplicate exception types as attributes
pass
try:
pass
except (IOError, EnvironmentError, OSError):
# Detect if a primary exception and any its aliases are present.
#
# Since Python 3.3, IOError, EnvironmentError, WindowsError, mmap.error,
# socket.error and select.error are aliases of OSError. See PEP 3151 for
# more info.
pass
try:
pass
except (MyException, NotImplemented):
# NotImplemented is not an exception, let's not crash on it.
pass
try:
pass
except (ValueError, binascii.Error):
# binascii.Error is a subclass of ValueError.
pass

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

@@ -0,0 +1,38 @@
"""
Should emit:
B025 - on lines 15, 22, 31
"""
import pickle
try:
a = 1
except ValueError:
a = 2
finally:
a = 3
try:
a = 1
except ValueError:
a = 2
except ValueError:
a = 2
try:
a = 1
except pickle.PickleError:
a = 2
except ValueError:
a = 2
except pickle.PickleError:
a = 2
try:
a = 1
except (ValueError, TypeError):
a = 2
except ValueError:
a = 2
except (OSError, TypeError):
a = 2

1
resources/test/fixtures/C400.py vendored Normal file
View File

@@ -0,0 +1 @@
x = list(x for x in range(3))

1
resources/test/fixtures/C401.py vendored Normal file
View File

@@ -0,0 +1 @@
x = set(x for x in range(3))

1
resources/test/fixtures/C402.py vendored Normal file
View File

@@ -0,0 +1 @@
d = dict((x, x) for x in range(3))

1
resources/test/fixtures/C403.py vendored Normal file
View File

@@ -0,0 +1 @@
s = set([x for x in range(3)])

1
resources/test/fixtures/C404.py vendored Normal file
View File

@@ -0,0 +1 @@
d = dict([(i, i) for i in range(3)])

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

@@ -0,0 +1,5 @@
s1 = set([1, 2])
s2 = set((1, 2))
s3 = set([])
s4 = set(())
s5 = set()

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

@@ -0,0 +1,5 @@
d1 = dict([(1, 2)])
d2 = dict(((1, 2),))
d3 = dict([])
d4 = dict(())
d5 = dict()

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

@@ -0,0 +1,5 @@
t = tuple()
l = list()
d1 = dict()
d2 = dict(a=1)
d3 = dict(**d2)

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

@@ -0,0 +1,3 @@
t1 = tuple([1, 2])
t2 = tuple((1, 2))
t3 = tuple([])

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

@@ -0,0 +1,4 @@
l1 = list([1, 2])
l2 = list((1, 2))
l3 = list([])
l4 = list(())

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

@@ -0,0 +1,14 @@
x = [1, 2, 3]
list(list(x))
list(tuple(x))
tuple(list(x))
tuple(tuple(x))
set(set(x))
set(list(x))
set(tuple(x))
set(sorted(x))
set(reversed(x))
sorted(list(x))
sorted(tuple(x))
sorted(sorted(x))
sorted(reversed(x))

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

@@ -0,0 +1,9 @@
lst = [2, 1, 3]
a = set(lst[::-1])
b = reversed(lst[::-1])
c = sorted(lst[::-1])
d = sorted(lst[::-1], reverse=True)
e = set(lst[2:-1])
f = set(lst[:1:-1])
g = set(lst[::1])
h = set(lst[::-2])

534
resources/test/fixtures/D.py vendored Normal file
View File

@@ -0,0 +1,534 @@
# No docstring, so we can test D100
from functools import wraps
import os
from .expected import Expectation
from typing import overload
expectation = Expectation()
expect = expectation.expect
expect('class_', 'D101: Missing docstring in public class')
class class_:
expect('meta', 'D419: Docstring is empty')
class meta:
""""""
@expect('D102: Missing docstring in public method')
def method(self=None):
pass
def _ok_since_private(self=None):
pass
@overload
def overloaded_method(self, a: int) -> str:
...
@overload
def overloaded_method(self, a: str) -> str:
"""Foo bar documentation."""
...
def overloaded_method(a):
"""Foo bar documentation."""
return str(a)
expect('overloaded_method',
"D418: Function/ Method decorated with @overload"
" shouldn't contain a docstring")
@property
def foo(self):
"""The foo of the thing, which isn't in imperitive mood."""
return "hello"
@expect('D102: Missing docstring in public method')
def __new__(self=None):
pass
@expect('D107: Missing docstring in __init__')
def __init__(self=None):
pass
@expect('D105: Missing docstring in magic method')
def __str__(self=None):
pass
@expect('D102: Missing docstring in public method')
def __call__(self=None, x=None, y=None, z=None):
pass
@expect('D419: Docstring is empty')
def function():
""" """
def ok_since_nested():
pass
@expect('D419: Docstring is empty')
def nested():
''
def function_with_nesting():
"""Foo bar documentation."""
@overload
def nested_overloaded_func(a: int) -> str:
...
@overload
def nested_overloaded_func(a: str) -> str:
"""Foo bar documentation."""
...
def nested_overloaded_func(a):
"""Foo bar documentation."""
return str(a)
expect('nested_overloaded_func',
"D418: Function/ Method decorated with @overload"
" shouldn't contain a docstring")
@overload
def overloaded_func(a: int) -> str:
...
@overload
def overloaded_func(a: str) -> str:
"""Foo bar documentation."""
...
def overloaded_func(a):
"""Foo bar documentation."""
return str(a)
expect('overloaded_func',
"D418: Function/ Method decorated with @overload"
" shouldn't contain a docstring")
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def asdlkfasd():
"""
Wrong.
"""
@expect('D201: No blank lines allowed before function docstring (found 1)')
def leading_space():
"""Leading space."""
@expect('D202: No blank lines allowed after function docstring (found 1)')
def trailing_space():
"""Leading space."""
pass
@expect('D201: No blank lines allowed before function docstring (found 1)')
@expect('D202: No blank lines allowed after function docstring (found 1)')
def trailing_and_leading_space():
"""Trailing and leading space."""
pass
expect('LeadingSpaceMissing',
'D203: 1 blank line required before class docstring (found 0)')
class LeadingSpaceMissing:
"""Leading space missing."""
expect('WithLeadingSpace',
'D211: No blank lines allowed before class docstring (found 1)')
class WithLeadingSpace:
"""With leading space."""
expect('TrailingSpace',
'D204: 1 blank line required after class docstring (found 0)')
expect('TrailingSpace',
'D211: No blank lines allowed before class docstring (found 1)')
class TrailingSpace:
"""TrailingSpace."""
pass
expect('LeadingAndTrailingSpaceMissing',
'D203: 1 blank line required before class docstring (found 0)')
expect('LeadingAndTrailingSpaceMissing',
'D204: 1 blank line required after class docstring (found 0)')
class LeadingAndTrailingSpaceMissing:
"""Leading and trailing space missing."""
pass
@expect('D205: 1 blank line required between summary line and description '
'(found 0)')
@expect('D213: Multi-line docstring summary should start at the second line')
def multi_line_zero_separating_blanks():
"""Summary.
Description.
"""
@expect('D205: 1 blank line required between summary line and description '
'(found 2)')
@expect('D213: Multi-line docstring summary should start at the second line')
def multi_line_two_separating_blanks():
"""Summary.
Description.
"""
@expect('D213: Multi-line docstring summary should start at the second line')
def multi_line_one_separating_blanks():
"""Summary.
Description.
"""
@expect('D207: Docstring is under-indented')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfsdf():
"""Summary.
Description.
"""
@expect('D207: Docstring is under-indented')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdsdfsdffsdf():
"""Summary.
Description.
"""
@expect('D208: Docstring is over-indented')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfsdsdf24():
"""Summary.
Description.
"""
@expect('D208: Docstring is over-indented')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfsdsdfsdf24():
"""Summary.
Description.
"""
@expect('D208: Docstring is over-indented')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfsdfsdsdsdfsdf24():
"""Summary.
Description.
"""
@expect('D209: Multi-line docstring closing quotes should be on a separate '
'line')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfljdf24():
"""Summary.
Description."""
@expect('D210: No whitespaces allowed surrounding docstring text')
def endswith():
"""Whitespace at the end. """
@expect('D210: No whitespaces allowed surrounding docstring text')
def around():
""" Whitespace at everywhere. """
@expect('D210: No whitespaces allowed surrounding docstring text')
@expect('D213: Multi-line docstring summary should start at the second line')
def multiline():
""" Whitespace at the beginning.
This is the end.
"""
@expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
def triple_single_quotes_raw():
r'''Summary.'''
@expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
def triple_single_quotes_raw_uppercase():
R'''Summary.'''
@expect('D300: Use """triple double quotes""" (found \'-quotes)')
def single_quotes_raw():
r'Summary.'
@expect('D300: Use """triple double quotes""" (found \'-quotes)')
def single_quotes_raw_uppercase():
R'Summary.'
@expect('D300: Use """triple double quotes""" (found \'-quotes)')
@expect('D301: Use r""" if any backslashes in a docstring')
def single_quotes_raw_uppercase_backslash():
R'Sum\mary.'
@expect('D301: Use r""" if any backslashes in a docstring')
def double_quotes_backslash():
"""Sum\\mary."""
@expect('D301: Use r""" if any backslashes in a docstring')
def double_quotes_backslash_uppercase():
R"""Sum\\mary."""
@expect('D213: Multi-line docstring summary should start at the second line')
def exceptions_of_D301():
"""Exclude some backslashes from D301.
In particular, line continuations \
and unicode literals \u0394 and \N{GREEK CAPITAL LETTER DELTA}.
They are considered to be intentionally unescaped.
"""
@expect("D400: First line should end with a period (not 'y')")
@expect("D415: First line should end with a period, question mark, "
"or exclamation point (not 'y')")
def lwnlkjl():
"""Summary"""
@expect("D401: First line should be in imperative mood "
"(perhaps 'Return', not 'Returns')")
def liouiwnlkjl():
"""Returns foo."""
@expect("D401: First line should be in imperative mood; try rephrasing "
"(found 'Constructor')")
def sdgfsdg23245():
"""Constructor for a foo."""
@expect("D401: First line should be in imperative mood; try rephrasing "
"(found 'Constructor')")
def sdgfsdg23245777():
"""Constructor."""
@expect('D402: First line should not be the function\'s "signature"')
def foobar():
"""Signature: foobar()."""
@expect('D213: Multi-line docstring summary should start at the second line')
def new_209():
"""First line.
More lines.
"""
pass
@expect('D213: Multi-line docstring summary should start at the second line')
def old_209():
"""One liner.
Multi-line comments. OK to have extra blank line
"""
@expect("D103: Missing docstring in public function")
def oneliner_d102(): return
@expect("D400: First line should end with a period (not 'r')")
@expect("D415: First line should end with a period, question mark,"
" or exclamation point (not 'r')")
def oneliner_withdoc(): """One liner"""
def ignored_decorator(func): # noqa: D400,D401,D415
"""Runs something"""
func()
pass
def decorator_for_test(func): # noqa: D400,D401,D415
"""Runs something"""
func()
pass
@ignored_decorator
def oneliner_ignored_decorator(): """One liner"""
@decorator_for_test
@expect("D400: First line should end with a period (not 'r')")
@expect("D415: First line should end with a period, question mark,"
" or exclamation point (not 'r')")
def oneliner_with_decorator_expecting_errors(): """One liner"""
@decorator_for_test
def valid_oneliner_with_decorator(): """One liner."""
@expect("D207: Docstring is under-indented")
@expect('D213: Multi-line docstring summary should start at the second line')
def docstring_start_in_same_line(): """First Line.
Second Line
"""
def function_with_lambda_arg(x=lambda y: y):
"""Wrap the given lambda."""
@expect('D213: Multi-line docstring summary should start at the second line')
def a_following_valid_function(x=None):
"""Check for a bug where the previous function caused an assertion.
The assertion was caused in the next function, so this one is necessary.
"""
def outer_function():
"""Do something."""
def inner_function():
"""Do inner something."""
return 0
@expect("D400: First line should end with a period (not 'g')")
@expect("D401: First line should be in imperative mood "
"(perhaps 'Run', not 'Runs')")
@expect("D415: First line should end with a period, question mark, "
"or exclamation point (not 'g')")
def docstring_bad():
"""Runs something"""
pass
def docstring_bad_ignore_all(): # noqa
"""Runs something"""
pass
def docstring_bad_ignore_one(): # noqa: D400,D401,D415
"""Runs something"""
pass
@expect("D401: First line should be in imperative mood "
"(perhaps 'Run', not 'Runs')")
def docstring_ignore_some_violations_but_catch_D401(): # noqa: E501,D400,D415
"""Runs something"""
pass
@expect(
"D401: First line should be in imperative mood "
"(perhaps 'Initiate', not 'Initiates')"
)
def docstring_initiates():
"""Initiates the process."""
@expect(
"D401: First line should be in imperative mood "
"(perhaps 'Initialize', not 'Initializes')"
)
def docstring_initializes():
"""Initializes the process."""
@wraps(docstring_bad_ignore_one)
def bad_decorated_function():
"""Bad (E501) but decorated"""
pass
def valid_google_string(): # noqa: D400
"""Test a valid something!"""
@expect("D415: First line should end with a period, question mark, "
"or exclamation point (not 'g')")
def bad_google_string(): # noqa: D400
"""Test a valid something"""
# This is reproducing a bug where AttributeError is raised when parsing class
# parameters as functions for Google / Numpy conventions.
class Blah: # noqa: D203,D213
"""A Blah.
Parameters
----------
x : int
"""
def __init__(self, x):
pass
expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]),
'D100: Missing docstring in public module')

View File

@@ -65,3 +65,24 @@ b = Union["Nut", None]
c = cast("Vegetable", b)
Field = lambda default=MISSING: field(default=default)
# Test: access a sub-importation via an alias.
import pyarrow as pa
import pyarrow.csv
print(pa.csv.read_csv("test.csv"))
# Test: referencing an import via TypeAlias.
import sys
import numpy as np
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
CustomInt: TypeAlias = "np.int8 | np.int16"

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

@@ -0,0 +1,5 @@
"""Access a sub-importation via an alias."""
import pyarrow as pa
import pyarrow.csv
print(pa.csv.read_csv("test.csv"))

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

@@ -0,0 +1,12 @@
"""Test: referencing an import via TypeAlias."""
import sys
import numpy as np
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
CustomInt: TypeAlias = "np.int8 | np.int16"

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

@@ -0,0 +1,14 @@
"""Test: referencing an import via TypeAlias (with future annotations)."""
from __future__ import annotations
import sys
import numpy as np
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
CustomInt: TypeAlias = np.int8 | np.int16

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

@@ -0,0 +1,14 @@
"""Test: referencing an import via TypeAlias (with future annotations and quotes)."""
from __future__ import annotations
import sys
import numpy as np
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
CustomInt: TypeAlias = "np.int8 | np.int16"

View File

@@ -1,4 +1,3 @@
from __future__ import print_function
"""Docstring"""
from __future__ import absolute_import

View File

@@ -89,3 +89,45 @@ A = (
f'B'
f'{B}'
)
from typing import Annotated, Literal
def arbitrary_callable() -> None:
...
class PEP593Test:
field: Annotated[
int,
"base64",
arbitrary_callable(),
123,
(1, 2, 3),
]
field_with_stringified_type: Annotated[
"PEP593Test",
123,
]
field_with_undefined_stringified_type: Annotated[
"PEP593Test123",
123,
]
field_with_nested_subscript: Annotated[
dict[Literal["foo"], str],
123,
]
field_with_undefined_nested_subscript: Annotated[
dict["foo", "bar"], # Expected to fail as undefined.
123,
]
def in_ipython_notebook() -> bool:
try:
# autoimported by notebooks
get_ipython() # type: ignore[name-defined]
except NameError:
return False # not in notebook
return True

View File

@@ -15,6 +15,9 @@ def f() -> None:
# Invalid
d = 1 # noqa: F841, E501
# Invalid (and unimplemented)
d = 1 # noqa: F841, W191
# Valid
_ = """Lorem ipsum dolor sit amet.

View File

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

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

@@ -0,0 +1,15 @@
from os.path import abspath
x = abspath(__file__)
import os
y = os.path.abspath(__file__)
from os import path
z = path.abspath(__file__)

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

@@ -0,0 +1,5 @@
type('')
type(b'')
type(0)
type(0.)
type(0j)

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

@@ -0,0 +1,10 @@
import unittest
class Suite(unittest.TestCase):
def test(self) -> None:
self.assertEquals (1, 2)
self.assertEquals(1, 2)
self.assertEqual(3, 4)
self.failUnlessAlmostEqual(1, 1.1)
self.assertNotRegexpMatches("a", "b")

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

@@ -0,0 +1,12 @@
from typing import List
def f(x: List[str]) -> None:
...
import typing
def f(x: typing.List[str]) -> None:
...

40
resources/test/fixtures/U007.py vendored Normal file
View File

@@ -0,0 +1,40 @@
from typing import Optional
def f(x: Optional[str]) -> None:
...
import typing
def f(x: typing.Optional[str]) -> None:
...
from typing import Union
def f(x: Union[str, int, Union[float, bytes]]) -> None:
...
import typing
def f(x: typing.Union[str, int]) -> None:
...
from typing import Union
def f(x: "Union[str, int, Union[float, bytes]]") -> None:
...
import typing
def f(x: "typing.Union[str, int]") -> None:
...

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

@@ -0,0 +1,2 @@
def fn() -> None:
pass

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

@@ -0,0 +1,2 @@
def fn() -> None:
pass # noqa: W292

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

@@ -0,0 +1,2 @@
def fn() -> None:
pass

497
resources/test/fixtures/sections.py vendored Normal file
View File

@@ -0,0 +1,497 @@
"""A valid module docstring."""
from .expected import Expectation
expectation = Expectation()
expect = expectation.expect
_D213 = 'D213: Multi-line docstring summary should start at the second line'
_D400 = "D400: First line should end with a period (not '!')"
@expect(_D213)
@expect("D405: Section name should be properly capitalized "
"('Returns', not 'returns')")
def not_capitalized(): # noqa: D416
"""Toggle the gizmo.
returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D406: Section name should end with a newline "
"('Returns', not 'Returns:')")
def superfluous_suffix(): # noqa: D416
"""Toggle the gizmo.
Returns:
-------
A value of some sort.
"""
@expect(_D213)
@expect("D407: Missing dashed underline after section ('Returns')")
def no_underline(): # noqa: D416
"""Toggle the gizmo.
Returns
A value of some sort.
"""
@expect(_D213)
@expect("D407: Missing dashed underline after section ('Returns')")
@expect("D414: Section has no content ('Returns')")
def no_underline_and_no_description(): # noqa: D416
"""Toggle the gizmo.
Returns
"""
@expect(_D213)
@expect("D410: Missing blank line after section ('Returns')")
@expect("D414: Section has no content ('Returns')")
@expect("D411: Missing blank line before section ('Yields')")
@expect("D414: Section has no content ('Yields')")
def consecutive_sections(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
Yields
------
Raises
------
Questions.
"""
@expect(_D213)
@expect("D408: Section underline should be in the line following the "
"section's name ('Returns')")
def blank_line_before_underline(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D409: Section underline should match the length of its name "
"(Expected 7 dashes in section 'Returns', got 2)")
def bad_underline_length(): # noqa: D416
"""Toggle the gizmo.
Returns
--
A value of some sort.
"""
@expect(_D213)
@expect("D413: Missing blank line after last section ('Returns')")
def no_blank_line_after_last_section(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D411: Missing blank line before section ('Returns')")
def no_blank_line_before_section(): # noqa: D416
"""Toggle the gizmo.
The function's description.
Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D214: Section is over-indented ('Returns')")
def section_overindented(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D215: Section underline is over-indented (in section 'Returns')")
def section_underline_overindented(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D215: Section underline is over-indented (in section 'Returns')")
@expect("D413: Missing blank line after last section ('Returns')")
@expect("D414: Section has no content ('Returns')")
def section_underline_overindented_and_contentless(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
"""
@expect(_D213)
def ignore_non_actual_section(): # noqa: D416
"""Toggle the gizmo.
This is the function's description, which will also specify what it
returns
"""
@expect(_D213)
@expect("D401: First line should be in imperative mood "
"(perhaps 'Return', not 'Returns')")
@expect("D400: First line should end with a period (not 's')")
@expect("D415: First line should end with a period, question "
"mark, or exclamation point (not 's')")
@expect("D205: 1 blank line required between summary line and description "
"(found 0)")
def section_name_in_first_line(): # noqa: D416
"""Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D405: Section name should be properly capitalized "
"('Short Summary', not 'Short summary')")
@expect("D412: No blank lines allowed between a section header and its "
"content ('Short Summary')")
@expect("D409: Section underline should match the length of its name "
"(Expected 7 dashes in section 'Returns', got 6)")
@expect("D410: Missing blank line after section ('Returns')")
@expect("D411: Missing blank line before section ('Raises')")
@expect("D406: Section name should end with a newline "
"('Raises', not 'Raises:')")
@expect("D407: Missing dashed underline after section ('Raises')")
def multiple_sections(): # noqa: D416
"""Toggle the gizmo.
Short summary
-------------
This is the function's description, which will also specify what it
returns.
Returns
------
Many many wonderful things.
Raises:
My attention.
"""
@expect(_D213)
def false_positive_section_prefix(): # noqa: D416
"""Toggle the gizmo.
Parameters
----------
attributes_are_fun: attributes for the function.
"""
@expect(_D213)
def section_names_as_parameter_names(): # noqa: D416
"""Toggle the gizmo.
Parameters
----------
notes : list
A list of wonderful notes.
examples: list
A list of horrible examples.
"""
@expect(_D213)
@expect("D414: Section has no content ('Returns')")
def valid_google_style_section(): # noqa: D406, D407
"""Toggle the gizmo.
Args:
note: A random string.
Returns:
Raises:
RandomError: A random error that occurs randomly.
"""
@expect(_D213)
@expect("D416: Section name should end with a colon "
"('Args:', not 'Args')")
def missing_colon_google_style_section(): # noqa: D406, D407
"""Toggle the gizmo.
Args
note: A random string.
"""
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) y are missing descriptions in "
"'bar' docstring)", func_name="bar")
def _test_nested_functions():
x = 1
def bar(y=2): # noqa: D207, D213, D406, D407
"""Nested function test for docstrings.
Will this work when referencing x?
Args:
x: Test something
that is broken.
"""
print(x)
@expect(_D213)
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) y are missing descriptions in "
"'test_missing_google_args' docstring)")
def test_missing_google_args(x=1, y=2, _private=3): # noqa: D406, D407
"""Toggle the gizmo.
Args:
x (int): The greatest integer.
"""
class TestGoogle: # noqa: D203
"""Test class."""
def test_method(self, test, another_test, _): # noqa: D213, D407
"""Test a valid args section.
Args:
test: A parameter.
another_test: Another parameter.
"""
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) test, y, z are missing descriptions in "
"'test_missing_args' docstring)", arg_count=5)
def test_missing_args(self, test, x, y, z=3, _private_arg=3): # noqa: D213, D407
"""Test a valid args section.
Args:
x: Another parameter.
"""
@classmethod
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) test, y, z are missing descriptions in "
"'test_missing_args_class_method' docstring)", arg_count=5)
def test_missing_args_class_method(cls, test, x, y, _, z=3): # noqa: D213, D407
"""Test a valid args section.
Args:
x: Another parameter. The parameter below is missing description.
y:
"""
@staticmethod
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) a, y, z are missing descriptions in "
"'test_missing_args_static_method' docstring)", arg_count=4)
def test_missing_args_static_method(a, x, y, _test, z=3): # noqa: D213, D407
"""Test a valid args section.
Args:
x: Another parameter.
"""
@staticmethod
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) a, b are missing descriptions in "
"'test_missing_docstring' docstring)", arg_count=2)
def test_missing_docstring(a, b): # noqa: D213, D407
"""Test a valid args section.
Args:
a:
"""
@staticmethod
def test_hanging_indent(skip, verbose): # noqa: D213, D407
"""Do stuff.
Args:
skip (:attr:`.Skip`):
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Etiam at tellus a tellus faucibus maximus. Curabitur tellus
mauris, semper id vehicula ac, feugiat ut tortor.
verbose (bool):
If True, print out as much infromation as possible.
If False, print out concise "one-liner" information.
"""
@expect(_D213)
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) y are missing descriptions in "
"'test_missing_numpy_args' docstring)")
def test_missing_numpy_args(_private_arg=0, x=1, y=2): # noqa: D406, D407
"""Toggle the gizmo.
Parameters
----------
x : int
The greatest integer in the history \
of the entire world.
"""
class TestNumpy: # noqa: D203
"""Test class."""
def test_method(self, test, another_test, z, _, x=1, y=2, _private_arg=1): # noqa: D213, D407
"""Test a valid args section.
Some long string with a \
line continuation.
Parameters
----------
test, another_test
Some parameters without type.
z : some parameter with a very long type description that requires a \
line continuation.
But no further description.
x, y : int
Some integer parameters.
"""
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) test, y, z are missing descriptions in "
"'test_missing_args' docstring)", arg_count=5)
def test_missing_args(self, test, x, y, z=3, t=1, _private=0): # noqa: D213, D407
"""Test a valid args section.
Parameters
----------
x, t : int
Some parameters.
"""
@classmethod
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) test, y, z are missing descriptions in "
"'test_missing_args_class_method' docstring)", arg_count=4)
def test_missing_args_class_method(cls, test, x, y, z=3): # noqa: D213, D407
"""Test a valid args section.
Parameters
----------
z
x
Another parameter. The parameters y, test below are
missing descriptions. The parameter z above is also missing
a description.
y
test
"""
@staticmethod
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) a, z are missing descriptions in "
"'test_missing_args_static_method' docstring)", arg_count=3)
def test_missing_args_static_method(a, x, y, z=3, t=1): # noqa: D213, D407
"""Test a valid args section.
Parameters
----------
x, y
Another parameter.
t : int
Yet another parameter.
"""
@staticmethod
def test_mixing_numpy_and_google(danger): # noqa: D213
"""Repro for #388.
Parameters
----------
danger
Zoneeeeee!
"""
class TestIncorrectIndent: # noqa: D203
"""Test class."""
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) y are missing descriptions in "
"'test_incorrect_indent' docstring)", arg_count=3)
def test_incorrect_indent(self, x=1, y=2): # noqa: D207, D213, D407
"""Reproducing issue #437.
Testing this incorrectly indented docstring.
Args:
x: Test argument.
"""

23
setup.py Normal file
View File

@@ -0,0 +1,23 @@
import sys
from setuptools import setup
sys.stderr.write(
"""
===============================
Unsupported installation method
===============================
ruff no longer supports installation with `python setup.py install`.
Please use `python -m pip install .` instead.
"""
)
sys.exit(1)
# The below code will never execute, however GitHub is particularly
# picky about where it finds Python packaging metadata.
# See: https://github.com/github/feedback/discussions/6456
#
# To be removed once GitHub catches up.
setup(name="ruff", install_requires=[])

View File

@@ -1,4 +1,5 @@
pub mod checks;
pub mod checkers;
pub mod helpers;
pub mod operations;
pub mod relocate;
pub mod types;

View File

@@ -1,22 +1,23 @@
use std::collections::BTreeSet;
use itertools::izip;
use num_bigint::BigInt;
use regex::Regex;
use rustpython_parser::ast::{
Arg, ArgData, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind,
Keyword, Location, Stmt, StmtKind, Unaryop,
KeywordData, Located, Stmt, StmtKind, Unaryop,
};
use serde::{Deserialize, Serialize};
use crate::ast::operations::SourceCodeLocator;
use crate::ast::helpers;
use crate::ast::types::{
Binding, BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind,
};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckKind, Fix, RejectedCmpop};
use crate::checks::{Check, CheckKind, RejectedCmpop};
use crate::python::builtins::BUILTINS;
/// Check IfTuple compliance.
pub fn check_if_tuple(test: &Expr, location: Range) -> Option<Check> {
pub fn if_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
return Some(Check::new(CheckKind::IfTuple, location));
@@ -26,7 +27,7 @@ pub fn check_if_tuple(test: &Expr, location: Range) -> Option<Check> {
}
/// Check AssertTuple compliance.
pub fn check_assert_tuple(test: &Expr, location: Range) -> Option<Check> {
pub fn assert_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
return Some(Check::new(CheckKind::AssertTuple, location));
@@ -36,7 +37,7 @@ pub fn check_assert_tuple(test: &Expr, location: Range) -> Option<Check> {
}
/// Check NotInTest and NotIsTest compliance.
pub fn check_not_tests(
pub fn not_tests(
op: &Unaryop,
operand: &Expr,
check_not_in: bool,
@@ -75,7 +76,7 @@ pub fn check_not_tests(
}
/// Check UnusedVariable compliance.
pub fn check_unused_variables(
pub fn unused_variables(
scope: &Scope,
locator: &dyn CheckLocator,
dummy_variable_rgx: &Regex,
@@ -108,7 +109,7 @@ pub fn check_unused_variables(
}
/// Check DoNotAssignLambda compliance.
pub fn check_do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
pub fn do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Lambda { .. } = &value.node {
Some(Check::new(CheckKind::DoNotAssignLambda, location))
} else {
@@ -117,11 +118,7 @@ pub fn check_do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check
}
/// Check UselessMetaclassType compliance.
pub fn check_useless_metaclass_type(
targets: &Vec<Expr>,
value: &Expr,
location: Range,
) -> Option<Check> {
pub fn useless_metaclass_type(targets: &Vec<Expr>, value: &Expr, location: Range) -> Option<Check> {
if targets.len() == 1 {
if let ExprKind::Name { id, .. } = targets.first().map(|expr| &expr.node).unwrap() {
if id == "__metaclass__" {
@@ -136,12 +133,92 @@ pub fn check_useless_metaclass_type(
None
}
/// Check UnnecessaryAbspath compliance.
pub fn unnecessary_abspath(func: &Expr, args: &Vec<Expr>, location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &args[0].node {
if id == "__file__" {
match &func.node {
ExprKind::Attribute { attr: id, .. } | ExprKind::Name { id, .. } => {
if id == "abspath" {
return Some(Check::new(CheckKind::UnnecessaryAbspath, location));
}
}
_ => {}
}
}
}
}
None
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Primitive {
Bool,
Str,
Bytes,
Int,
Float,
Complex,
}
impl Primitive {
fn from_constant(constant: &Constant) -> Option<Self> {
match constant {
Constant::Bool(_) => Some(Primitive::Bool),
Constant::Str(_) => Some(Primitive::Str),
Constant::Bytes(_) => Some(Primitive::Bytes),
Constant::Int(_) => Some(Primitive::Int),
Constant::Float(_) => Some(Primitive::Float),
Constant::Complex { .. } => Some(Primitive::Complex),
_ => None,
}
}
pub fn builtin(&self) -> String {
match self {
Primitive::Bool => "bool".to_string(),
Primitive::Str => "str".to_string(),
Primitive::Bytes => "bytes".to_string(),
Primitive::Int => "int".to_string(),
Primitive::Float => "float".to_string(),
Primitive::Complex => "complex".to_string(),
}
}
}
/// Check TypeOfPrimitive compliance.
pub fn type_of_primitive(func: &Expr, args: &Vec<Expr>, location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {
match &func.node {
ExprKind::Attribute { attr: id, .. } | ExprKind::Name { id, .. } => {
if id == "type" {
if let ExprKind::Constant { value, .. } = &args[0].node {
if let Some(primitive) = Primitive::from_constant(value) {
return Some(Check::new(
CheckKind::TypeOfPrimitive(primitive),
location,
));
}
}
}
}
_ => {}
}
}
None
}
fn is_ambiguous_name(name: &str) -> bool {
name == "l" || name == "I" || name == "O"
}
/// Check AmbiguousVariableName compliance.
pub fn check_ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
pub fn ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousVariableName(name.to_string()),
@@ -153,7 +230,7 @@ pub fn check_ambiguous_variable_name(name: &str, location: Range) -> Option<Chec
}
/// Check AmbiguousClassName compliance.
pub fn check_ambiguous_class_name(name: &str, location: Range) -> Option<Check> {
pub fn ambiguous_class_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousClassName(name.to_string()),
@@ -165,7 +242,7 @@ pub fn check_ambiguous_class_name(name: &str, location: Range) -> Option<Check>
}
/// Check AmbiguousFunctionName compliance.
pub fn check_ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
pub fn ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousFunctionName(name.to_string()),
@@ -177,15 +254,7 @@ pub fn check_ambiguous_function_name(name: &str, location: Range) -> Option<Chec
}
/// Check UselessObjectInheritance compliance.
pub fn check_useless_object_inheritance(
stmt: &Stmt,
name: &str,
bases: &[Expr],
keywords: &[Keyword],
scope: &Scope,
locator: &mut SourceCodeLocator,
autofix: &fixer::Mode,
) -> Option<Check> {
pub fn useless_object_inheritance(name: &str, bases: &[Expr], scope: &Scope) -> Option<Check> {
for expr in bases {
if let ExprKind::Name { id, .. } = &expr.node {
if id == "object" {
@@ -195,22 +264,10 @@ pub fn check_useless_object_inheritance(
kind: BindingKind::Builtin,
..
}) => {
let mut check = Check::new(
return Some(Check::new(
CheckKind::UselessObjectInheritance(name.to_string()),
Range::from_located(expr),
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_class_def_base(
locator,
&stmt.location,
expr.location,
bases,
keywords,
) {
check.amend(fix);
}
}
return Some(check);
));
}
_ => {}
}
@@ -222,7 +279,7 @@ pub fn check_useless_object_inheritance(
}
/// Check DefaultExceptNotLast compliance.
pub fn check_default_except_not_last(handlers: &Vec<Excepthandler>) -> Option<Check> {
pub fn default_except_not_last(handlers: &Vec<Excepthandler>) -> Option<Check> {
for (idx, handler) in handlers.iter().enumerate() {
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
if type_.is_none() && idx < handlers.len() - 1 {
@@ -237,7 +294,7 @@ pub fn check_default_except_not_last(handlers: &Vec<Excepthandler>) -> Option<Ch
}
/// Check RaiseNotImplemented compliance.
pub fn check_raise_not_implemented(expr: &Expr) -> Option<Check> {
pub fn raise_not_implemented(expr: &Expr) -> Option<Check> {
match &expr.node {
ExprKind::Call { func, .. } => {
if let ExprKind::Name { id, .. } = &func.node {
@@ -264,7 +321,7 @@ pub fn check_raise_not_implemented(expr: &Expr) -> Option<Check> {
}
/// Check DuplicateArgumentName compliance.
pub fn check_duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
pub fn duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
// Collect all the arguments into a single vector.
@@ -297,36 +354,6 @@ pub fn check_duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
checks
}
/// Check AssertEquals compliance.
pub fn check_assert_equals(expr: &Expr, autofix: &fixer::Mode) -> Option<Check> {
if let ExprKind::Attribute { value, attr, .. } = &expr.node {
if attr == "assertEquals" {
if let ExprKind::Name { id, .. } = &value.node {
if id == "self" {
let mut check =
Check::new(CheckKind::NoAssertEquals, Range::from_located(expr));
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "assertEqual".to_string(),
location: Location::new(
expr.location.row(),
expr.location.column() + 1,
),
end_location: Location::new(
expr.location.row(),
expr.location.column() + 1 + "assertEquals".len(),
),
applied: false,
});
}
return Some(check);
}
}
}
}
None
}
#[derive(Debug, PartialEq)]
enum DictionaryKey<'a> {
Constant(&'a Constant),
@@ -342,7 +369,7 @@ fn convert_to_value(expr: &Expr) -> Option<DictionaryKey> {
}
/// Check MultiValueRepeatedKeyLiteral and MultiValueRepeatedKeyVariable compliance.
pub fn check_repeated_keys(
pub fn repeated_keys(
keys: &Vec<Expr>,
check_repeated_literals: bool,
check_repeated_variables: bool,
@@ -382,7 +409,7 @@ pub fn check_repeated_keys(
}
/// Check TrueFalseComparison and NoneComparison compliance.
pub fn check_literal_comparisons(
pub fn literal_comparisons(
left: &Expr,
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
@@ -513,7 +540,7 @@ fn is_constant_non_singleton(expr: &Expr) -> bool {
}
/// Check IsLiteral compliance.
pub fn check_is_literal(
pub fn is_literal(
left: &Expr,
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
@@ -535,11 +562,7 @@ pub fn check_is_literal(
}
/// Check TypeComparison compliance.
pub fn check_type_comparison(
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
location: Range,
) -> Vec<Check> {
pub fn type_comparison(ops: &Vec<Cmpop>, comparators: &Vec<Expr>, location: Range) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for (op, right) in izip!(ops, comparators) {
@@ -575,7 +598,7 @@ pub fn check_type_comparison(
}
/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance.
pub fn check_starred_expressions(
pub fn starred_expressions(
elts: &[Expr],
check_too_many_expressions: bool,
check_two_starred_expressions: bool,
@@ -605,7 +628,7 @@ pub fn check_starred_expressions(
}
/// Check BreakOutsideLoop compliance.
pub fn check_break_outside_loop(
pub fn break_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
@@ -646,7 +669,7 @@ pub fn check_break_outside_loop(
}
/// Check ContinueOutsideLoop compliance.
pub fn check_continue_outside_loop(
pub fn continue_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
@@ -694,11 +717,7 @@ pub enum ShadowingType {
}
/// Check builtin name shadowing
pub fn check_builtin_shadowing(
name: &str,
location: Range,
node_type: ShadowingType,
) -> Option<Check> {
pub fn builtin_shadowing(name: &str, location: Range, node_type: ShadowingType) -> Option<Check> {
if BUILTINS.contains(&name) {
Some(Check::new(
match node_type {
@@ -713,26 +732,388 @@ pub fn check_builtin_shadowing(
}
}
/// Returns `true` if a call is an argumented `super` invocation.
pub fn is_super_call_with_arguments(func: &Expr, args: &Vec<Expr>) -> bool {
// Check: is this a `super` call?
if let ExprKind::Name { id, .. } = &func.node {
id == "super" && !args.is_empty()
} else {
false
// flake8-comprehensions
/// Check `list(generator)` compliance.
pub fn unnecessary_generator_list(expr: &Expr, func: &Expr, args: &Vec<Expr>) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "list" {
if let ExprKind::GeneratorExp { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryGeneratorList,
Range::from_located(expr),
));
}
}
}
}
None
}
/// Check `set(generator)` compliance.
pub fn unnecessary_generator_set(expr: &Expr, func: &Expr, args: &Vec<Expr>) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" {
if let ExprKind::GeneratorExp { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryGeneratorList,
Range::from_located(expr),
));
}
}
}
}
None
}
/// Check `dict((x, y) for x, y in iterable)` compliance.
pub fn unnecessary_generator_dict(expr: &Expr, func: &Expr, args: &Vec<Expr>) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "dict" {
if let ExprKind::GeneratorExp { elt, .. } = &args[0].node {
match &elt.node {
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionDict,
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
}
None
}
/// Check `set([...])` compliance.
pub fn unnecessary_list_comprehension_set(
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" {
if let ExprKind::ListComp { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionSet,
Range::from_located(expr),
));
}
}
}
}
None
}
/// Check `dict([...])` compliance.
pub fn unnecessary_list_comprehension_dict(
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "dict" {
if let ExprKind::ListComp { elt, .. } = &args[0].node {
match &elt.node {
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionDict,
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
}
None
}
/// Check `set([1, 2])` compliance.
pub fn unnecessary_literal_set(expr: &Expr, func: &Expr, args: &Vec<Expr>) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" {
match &args[0].node {
ExprKind::List { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralSet("list".to_string()),
Range::from_located(expr),
));
}
ExprKind::Tuple { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralSet("tuple".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
None
}
/// Check `dict([(1, 2)])` compliance.
pub fn unnecessary_literal_dict(expr: &Expr, func: &Expr, args: &Vec<Expr>) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "dict" {
match &args[0].node {
ExprKind::Tuple { elts, .. } => {
if let Some(elt) = elts.first() {
match &elt.node {
// dict((1, 2), ...))
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralDict("tuple".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
} else {
// dict(())
return Some(Check::new(
CheckKind::UnnecessaryLiteralDict("tuple".to_string()),
Range::from_located(expr),
));
}
}
ExprKind::List { elts, .. } => {
if let Some(elt) = elts.first() {
match &elt.node {
// dict([(1, 2), ...])
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralDict("list".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
} else {
// dict([])
return Some(Check::new(
CheckKind::UnnecessaryLiteralDict("list".to_string()),
Range::from_located(expr),
));
}
}
_ => {}
}
}
}
}
None
}
pub fn unnecessary_collection_call(
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
keywords: &Vec<Located<KeywordData>>,
) -> Option<Check> {
if args.is_empty() {
if let ExprKind::Name { id, .. } = &func.node {
if id == "list" || id == "tuple" {
// list() or tuple()
return Some(Check::new(
CheckKind::UnnecessaryCollectionCall(id.to_string()),
Range::from_located(expr),
));
} else if id == "dict" {
// dict() or dict(a=1)
if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) {
return Some(Check::new(
CheckKind::UnnecessaryCollectionCall(id.to_string()),
Range::from_located(expr),
));
}
}
}
}
None
}
pub fn unnecessary_literal_within_tuple_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "tuple" {
if let Some(arg) = args.first() {
match &arg.node {
ExprKind::Tuple { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinTupleCall("tuple".to_string()),
Range::from_located(expr),
));
}
ExprKind::List { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinTupleCall("list".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
None
}
pub fn unnecessary_literal_within_list_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "list" {
if let Some(arg) = args.first() {
match &arg.node {
ExprKind::Tuple { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinListCall("tuple".to_string()),
Range::from_located(expr),
));
}
ExprKind::List { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinListCall("list".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
None
}
pub fn unnecessary_double_cast_or_process(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if let ExprKind::Name { id: outer, .. } = &func.node {
if outer == "list"
|| outer == "tuple"
|| outer == "set"
|| outer == "reversed"
|| outer == "sorted"
{
if let Some(arg) = args.first() {
if let ExprKind::Call { func, .. } = &arg.node {
if let ExprKind::Name { id: inner, .. } = &func.node {
// Ex) set(tuple(...))
if (outer == "set" || outer == "sorted")
&& (inner == "list"
|| inner == "tuple"
|| inner == "reversed"
|| inner == "sorted")
{
return Some(Check::new(
CheckKind::UnnecessaryDoubleCastOrProcess(
inner.to_string(),
outer.to_string(),
),
Range::from_located(expr),
));
}
// Ex) list(tuple(...))
if (outer == "list" || outer == "tuple")
&& (inner == "list" || inner == "tuple")
{
return Some(Check::new(
CheckKind::UnnecessaryDoubleCastOrProcess(
inner.to_string(),
outer.to_string(),
),
Range::from_located(expr),
));
}
// Ex) set(set(...))
if outer == "set" && inner == "set" {
return Some(Check::new(
CheckKind::UnnecessaryDoubleCastOrProcess(
inner.to_string(),
outer.to_string(),
),
Range::from_located(expr),
));
}
}
}
}
}
}
None
}
pub fn unnecessary_subscript_reversal(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if let Some(first_arg) = args.first() {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" || id == "sorted" || id == "reversed" {
if let ExprKind::Subscript { slice, .. } = &first_arg.node {
if let ExprKind::Slice { lower, upper, step } = &slice.node {
if lower.is_none() && upper.is_none() {
if let Some(step) = step {
if let ExprKind::UnaryOp {
op: Unaryop::USub,
operand,
} = &step.node
{
if let ExprKind::Constant {
value: Constant::Int(val),
..
} = &operand.node
{
if *val == BigInt::from(1) {
return Some(Check::new(
CheckKind::UnnecessarySubscriptReversal(
id.to_string(),
),
Range::from_located(expr),
));
}
}
}
}
}
}
}
}
}
}
None
}
// flake8-super
/// Check that `super()` has no args
pub fn check_super_args(
pub fn super_args(
scope: &Scope,
parents: &[&Stmt],
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) -> Option<Check> {
if !is_super_call_with_arguments(func, args) {
if !helpers::is_super_call_with_arguments(func, args) {
return None;
}
@@ -791,11 +1172,16 @@ pub fn check_super_args(
// flake8-print
/// Check whether a function call is a `print` or `pprint` invocation
pub fn check_print_call(expr: &Expr, func: &Expr) -> Option<Check> {
pub fn print_call(
expr: &Expr,
func: &Expr,
check_print: bool,
check_pprint: bool,
) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "print" {
if check_print && id == "print" {
return Some(Check::new(CheckKind::PrintFound, Range::from_located(expr)));
} else if id == "pprint" {
} else if check_pprint && id == "pprint" {
return Some(Check::new(
CheckKind::PPrintFound,
Range::from_located(expr),
@@ -805,7 +1191,7 @@ pub fn check_print_call(expr: &Expr, func: &Expr) -> Option<Check> {
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if let ExprKind::Name { id, .. } = &value.node {
if id == "pprint" && attr == "pprint" {
if check_pprint && id == "pprint" && attr == "pprint" {
return Some(Check::new(
CheckKind::PPrintFound,
Range::from_located(expr),

133
src/ast/helpers.rs Normal file
View File

@@ -0,0 +1,133 @@
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, StmtKind};
use crate::python::typing;
fn compose_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
match &expr.node {
ExprKind::Call { func, .. } => {
compose_call_path_inner(func, parts);
}
ExprKind::Attribute { value, attr, .. } => {
compose_call_path_inner(value, parts);
parts.push(attr);
}
ExprKind::Name { id, .. } => {
parts.push(id);
}
_ => {}
}
}
pub fn compose_call_path(expr: &Expr) -> Option<String> {
let mut segments = vec![];
compose_call_path_inner(expr, &mut segments);
if segments.is_empty() {
None
} else {
Some(segments.join("."))
}
}
pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
match &expr.node {
ExprKind::Attribute { attr, .. } => target == attr,
ExprKind::Name { id, .. } => target == id,
_ => false,
}
}
pub enum SubscriptKind {
AnnotatedSubscript,
PEP593AnnotatedSubscript,
}
pub fn match_annotated_subscript(expr: &Expr) -> Option<SubscriptKind> {
match &expr.node {
ExprKind::Attribute { attr, .. } => {
if typing::is_annotated_subscript(attr) {
Some(SubscriptKind::AnnotatedSubscript)
} else if typing::is_pep593_annotated_subscript(attr) {
Some(SubscriptKind::PEP593AnnotatedSubscript)
} else {
None
}
}
ExprKind::Name { id, .. } => {
if typing::is_annotated_subscript(id) {
Some(SubscriptKind::AnnotatedSubscript)
} else if typing::is_pep593_annotated_subscript(id) {
Some(SubscriptKind::PEP593AnnotatedSubscript)
} else {
None
}
}
_ => None,
}
}
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
// Check whether it's an assignment to a dunder, with or without a type annotation.
// This is what pycodestyle (as of 2.9.1) does.
match node {
StmtKind::Assign {
targets,
value: _,
type_comment: _,
} => {
if targets.len() != 1 {
return false;
}
match &targets[0].node {
ExprKind::Name { id, ctx: _ } => DUNDER_REGEX.is_match(id),
_ => false,
}
}
StmtKind::AnnAssign {
target,
annotation: _,
value: _,
simple: _,
} => match &target.node {
ExprKind::Name { id, ctx: _ } => DUNDER_REGEX.is_match(id),
_ => false,
},
_ => false,
}
}
/// Extract the names of all handled exceptions.
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<String> {
let mut handler_names = vec![];
for handler in handlers {
match &handler.node {
ExcepthandlerKind::ExceptHandler { type_, .. } => {
if let Some(type_) = type_ {
if let ExprKind::Tuple { elts, .. } = &type_.node {
for type_ in elts {
if let Some(name) = compose_call_path(type_) {
handler_names.push(name);
}
}
} else if let Some(name) = compose_call_path(type_) {
handler_names.push(name);
}
}
}
}
}
handler_names
}
/// Returns `true` if a call is an argumented `super` invocation.
pub fn is_super_call_with_arguments(func: &Expr, args: &Vec<Expr>) -> bool {
// Check: is this a `super` call?
if let ExprKind::Name { id, .. } = &func.node {
id == "super" && !args.is_empty()
} else {
false
}
}

View File

@@ -134,7 +134,7 @@ impl<'a> SourceCodeLocator<'a> {
}
}
pub fn slice_source_code_at(&mut self, location: &Location) -> &'a str {
fn init(&mut self) {
if !self.initialized {
let mut offset = 0;
for i in self.content.lines() {
@@ -142,24 +142,40 @@ impl<'a> SourceCodeLocator<'a> {
offset += i.len();
offset += 1;
}
self.offsets.push(offset);
self.initialized = true;
}
}
pub fn slice_source_code_at(&mut self, location: &Location) -> &'a str {
self.init();
let offset = self.offsets[location.row() - 1] + location.column() - 1;
&self.content[offset..]
}
pub fn slice_source_code_range(&mut self, range: &Range) -> &'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;
}
self.init();
let start = self.offsets[range.location.row() - 1] + range.location.column() - 1;
let end = self.offsets[range.end_location.row() - 1] + range.end_location.column() - 1;
&self.content[start..end]
}
pub fn partition_source_code_at(
&mut self,
outer: &Range,
inner: &Range,
) -> (&'a str, &'a str, &'a str) {
self.init();
let outer_start = self.offsets[outer.location.row() - 1] + outer.location.column() - 1;
let outer_end =
self.offsets[outer.end_location.row() - 1] + outer.end_location.column() - 1;
let inner_start = self.offsets[inner.location.row() - 1] + inner.location.column() - 1;
let inner_end =
self.offsets[inner.end_location.row() - 1] + inner.end_location.column() - 1;
(
&self.content[outer_start..inner_start],
&self.content[inner_start..inner_end],
&self.content[inner_end..outer_end],
)
}
}

View File

@@ -74,9 +74,9 @@ pub enum BindingKind {
Export(Vec<String>),
FutureImportation,
StarImportation,
Importation(String, BindingContext),
FromImportation(String, BindingContext),
SubmoduleImportation(String, BindingContext),
Importation(String, String, BindingContext),
FromImportation(String, String, BindingContext),
SubmoduleImportation(String, String, BindingContext),
}
#[derive(Clone, Debug)]

View File

@@ -4,6 +4,8 @@ use rustpython_parser::ast::{
PatternKind, Stmt, StmtKind, Unaryop, Withitem,
};
use crate::ast::helpers::match_name_or_attr;
pub trait Visitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
walk_stmt(self, stmt);
@@ -148,7 +150,11 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) {
} => {
visitor.visit_annotation(annotation);
if let Some(expr) = value {
visitor.visit_expr(expr);
if match_name_or_attr(annotation, "TypeAlias") {
visitor.visit_annotation(expr);
} else {
visitor.visit_expr(expr);
}
}
visitor.visit_expr(target);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
use std::collections::BTreeMap;
use crate::ast::types::Range;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::noqa;
@@ -66,12 +66,12 @@ pub fn check_lines(
match noqa {
(Directive::All(_, _), matches) => {
matches.push(check.kind.code().as_str());
matches.push(check.kind.code().as_ref());
ignored.push(index)
}
(Directive::Codes(_, _, codes), matches) => {
if codes.contains(&check.kind.code().as_str()) {
matches.push(check.kind.code().as_str());
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
ignored.push(index);
}
}
@@ -98,11 +98,11 @@ pub fn check_lines(
match noqa {
(Directive::All(_, _), matches) => {
matches.push(check.kind.code().as_str());
matches.push(check.kind.code().as_ref());
}
(Directive::Codes(_, _, codes), matches) => {
if codes.contains(&check.kind.code().as_str()) {
matches.push(check.kind.code().as_str());
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
} else {
line_checks.push(check);
}
@@ -113,6 +113,45 @@ pub fn check_lines(
}
}
// Enforce newlines at end of files.
if settings.enabled.contains(&CheckCode::W292) && !contents.ends_with('\n') {
// Note: if `lines.last()` is `None`, then `contents` is empty (and so we don't want to
// raise W292 anyway).
if let Some(line) = lines.last() {
let lineno = lines.len() - 1;
let noqa_lineno = noqa_line_for
.get(lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
let check = Check::new(
CheckKind::NoNewLineAtEndOfFile,
Range {
location: Location::new(lines.len(), line.len() + 1),
end_location: Location::new(lines.len(), line.len() + 1),
},
);
match noqa {
(Directive::All(_, _), matches) => {
matches.push(check.kind.code().as_ref());
}
(Directive::Codes(_, _, codes), matches) => {
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
} else {
line_checks.push(check);
}
}
(Directive::None, _) => line_checks.push(check),
}
}
}
// Enforce that the noqa directive was actually used.
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
@@ -145,15 +184,15 @@ pub fn check_lines(
let mut valid_codes = vec![];
for code in codes {
if !matches.contains(&code) {
invalid_codes.push(code);
invalid_codes.push(code.to_string());
} else {
valid_codes.push(code);
valid_codes.push(code.to_string());
}
}
if !invalid_codes.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(invalid_codes.join(", "))),
CheckKind::UnusedNOQA(Some(invalid_codes)),
Range {
location: Location::new(row + 1, start + 1),
end_location: Location::new(row + 1, end + 1),

File diff suppressed because it is too large Load Diff

139
src/cli.rs Normal file
View File

@@ -0,0 +1,139 @@
use std::fmt;
use std::path::PathBuf;
use clap::{command, Parser};
use log::warn;
use regex::Regex;
use crate::checks::CheckCode;
use crate::printer::SerializationFormat;
use crate::pyproject::StrCheckCodePair;
use crate::settings::PythonVersion;
use crate::RawSettings;
#[derive(Debug, Parser)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
#[command(version)]
pub struct Cli {
#[arg(required = true)]
pub files: Vec<PathBuf>,
/// Enable verbose logging.
#[arg(short, long)]
pub verbose: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[arg(short, long)]
pub quiet: bool,
/// Exit with status code "0", even upon detecting errors.
#[arg(short, long)]
pub exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
pub watch: bool,
/// Attempt to automatically fix lint errors.
#[arg(short, long)]
pub fix: bool,
/// Disable cache reads.
#[arg(short, long)]
pub no_cache: bool,
/// List of error codes to enable.
#[arg(long, value_delimiter = ',')]
pub select: Vec<CheckCode>,
/// Like --select, but adds additional error codes on top of the selected ones.
#[arg(long, value_delimiter = ',')]
pub extend_select: Vec<CheckCode>,
/// List of error codes to ignore.
#[arg(long, value_delimiter = ',')]
pub ignore: Vec<CheckCode>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[arg(long, value_delimiter = ',')]
pub extend_ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[arg(long, value_delimiter = ',')]
pub exclude: Vec<String>,
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[arg(long, value_delimiter = ',')]
pub extend_exclude: Vec<String>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
pub per_file_ignores: Vec<StrCheckCodePair>,
/// Output serialization format for error messages.
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
pub format: SerializationFormat,
/// See the files ruff will be run against with the current settings.
#[arg(long)]
pub show_files: bool,
/// See ruff's settings.
#[arg(long)]
pub show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
#[arg(long)]
pub add_noqa: bool,
/// Regular expression matching the name of dummy variables.
#[arg(long)]
pub dummy_variable_rgx: Option<Regex>,
/// The minimum Python version that should be supported.
#[arg(long)]
pub target_version: Option<PythonVersion>,
/// Round-trip auto-formatting.
// TODO(charlie): This should be a sub-command.
#[arg(long, hide = true)]
pub autoformat: bool,
/// The name of the file when passing it through stdin.
#[arg(long)]
pub stdin_filename: Option<String>,
}
pub enum Warnable {
Select,
ExtendSelect,
}
impl fmt::Display for Warnable {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Warnable::Select => fmt.write_str("--select"),
Warnable::ExtendSelect => fmt.write_str("--extend-select"),
}
}
}
/// Warn the user if they attempt to enable a code that won't be respected.
pub fn warn_on(
flag: Warnable,
codes: &Vec<CheckCode>,
cli_ignore: &Vec<CheckCode>,
cli_extend_ignore: &Vec<CheckCode>,
pyproject_settings: &RawSettings,
pyproject_path: &Option<PathBuf>,
) {
for code in codes {
if !cli_ignore.is_empty() {
if cli_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --ignore")
}
} else if pyproject_settings.ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `ignore` field in {}",
path.to_string_lossy()
)
} else {
warn!("{code:?} was passed to {flag}, but ignored by the default `ignore` field",)
}
}
if !cli_extend_ignore.is_empty() {
if cli_extend_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --extend-ignore")
}
} else if pyproject_settings.extend_ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `extend_ignore` field in {}",
path.to_string_lossy()
)
} else {
warn!("{code:?} was passed to {flag}, but ignored by the default `extend_ignore` field")
}
}
}
}

View File

@@ -115,7 +115,7 @@ impl SourceGenerator {
Ok(())
}
fn unparse_stmt<U>(&mut self, ast: &Stmt<U>) -> fmt::Result {
pub fn unparse_stmt<U>(&mut self, ast: &Stmt<U>) -> fmt::Result {
macro_rules! statement {
($body:block) => {{
self.newline()?;
@@ -547,7 +547,7 @@ impl SourceGenerator {
Ok(())
}
fn unparse_expr<U>(&mut self, ast: &Expr<U>, level: u8) -> fmt::Result {
pub fn unparse_expr<U>(&mut self, ast: &Expr<U>, level: u8) -> fmt::Result {
macro_rules! opprec {
($opty:ident, $x:expr, $enu:path, $($var:ident($op:literal, $prec:ident)),*$(,)?) => {
match $x {

3
src/docstrings.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod docstring_checks;
pub mod sections;
pub mod types;

View File

@@ -0,0 +1,676 @@
//! Abstractions for tracking and validating docstrings in Python code.
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::sections::{check_numpy_section, section_contexts};
use crate::docstrings::types::{Definition, DefinitionKind, Documentable};
use crate::visibility::{is_init, is_magic, is_overload, Modifier, Visibility, VisibleScope};
/// Extract a docstring from a function or class body.
pub fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
if let Some(stmt) = suite.first() {
if let StmtKind::Expr { value } = &stmt.node {
if matches!(
&value.node,
ExprKind::Constant {
value: Constant::Str(_),
..
}
) {
return Some(value);
}
}
}
None
}
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub fn extract<'a>(
scope: &VisibleScope,
stmt: &'a Stmt,
body: &'a [Stmt],
kind: &Documentable,
) -> Definition<'a> {
let expr = docstring_from(body);
match kind {
Documentable::Function => match scope {
VisibleScope {
modifier: Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Function(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
..
} => Definition {
kind: DefinitionKind::Method(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedFunction(stmt),
docstring: expr,
},
},
Documentable::Class => match scope {
VisibleScope {
modifier: Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Class(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
},
}
}
/// Extract the source code range for a docstring.
pub fn range_for(docstring: &Expr) -> Range {
// RustPython currently omits the first quotation mark in a string, so offset the location.
Range {
location: Location::new(docstring.location.row(), docstring.location.column() - 1),
end_location: docstring.end_location,
}
}
/// D100, D101, D102, D103, D104, D105, D106, D107
pub fn not_missing(
checker: &mut Checker,
definition: &Definition,
visibility: &Visibility,
) -> bool {
if matches!(visibility, Visibility::Private) {
return true;
}
if definition.docstring.is_some() {
return true;
}
match definition.kind {
DefinitionKind::Module => {
if checker.settings.enabled.contains(&CheckCode::D100) {
checker.add_check(Check::new(
CheckKind::PublicModule,
Range {
location: Location::new(1, 1),
end_location: Location::new(1, 1),
},
));
}
false
}
DefinitionKind::Package => {
if checker.settings.enabled.contains(&CheckCode::D104) {
checker.add_check(Check::new(
CheckKind::PublicPackage,
Range {
location: Location::new(1, 1),
end_location: Location::new(1, 1),
},
));
}
false
}
DefinitionKind::Class(stmt) => {
if checker.settings.enabled.contains(&CheckCode::D101) {
checker.add_check(Check::new(
CheckKind::PublicClass,
Range::from_located(stmt),
));
}
false
}
DefinitionKind::NestedClass(stmt) => {
if checker.settings.enabled.contains(&CheckCode::D106) {
checker.add_check(Check::new(
CheckKind::PublicNestedClass,
Range::from_located(stmt),
));
}
false
}
DefinitionKind::Function(stmt) | DefinitionKind::NestedFunction(stmt) => {
if is_overload(stmt) {
true
} else {
if checker.settings.enabled.contains(&CheckCode::D103) {
checker.add_check(Check::new(
CheckKind::PublicFunction,
Range::from_located(stmt),
));
}
false
}
}
DefinitionKind::Method(stmt) => {
if is_overload(stmt) {
true
} else if is_magic(stmt) {
if checker.settings.enabled.contains(&CheckCode::D105) {
checker.add_check(Check::new(
CheckKind::MagicMethod,
Range::from_located(stmt),
));
}
true
} else if is_init(stmt) {
if checker.settings.enabled.contains(&CheckCode::D107) {
checker.add_check(Check::new(CheckKind::PublicInit, Range::from_located(stmt)));
}
true
} else {
if checker.settings.enabled.contains(&CheckCode::D102) {
checker.add_check(Check::new(
CheckKind::PublicMethod,
Range::from_located(stmt),
));
}
true
}
}
}
}
/// D200
pub fn one_liner(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = &definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let mut line_count = 0;
let mut non_empty_line_count = 0;
for line in string.lines() {
line_count += 1;
if !line.trim().is_empty() {
non_empty_line_count += 1;
}
if non_empty_line_count > 1 {
break;
}
}
if non_empty_line_count == 1 && line_count > 1 {
checker.add_check(Check::new(CheckKind::FitsOnOneLine, range_for(docstring)));
}
}
}
}
static COMMENT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\s*#").unwrap());
static INNER_FUNCTION_OR_CLASS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s+(?:(?:class|def|async def)\s|@)").unwrap());
/// D201, D202
pub fn blank_before_after_function(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent)
| DefinitionKind::Method(parent) = &definition.kind
{
if let ExprKind::Constant {
value: Constant::Str(_),
..
} = &docstring.node
{
let (before, _, after) = checker
.locator
.partition_source_code_at(&Range::from_located(parent), &range_for(docstring));
if checker.settings.enabled.contains(&CheckCode::D201) {
let blank_lines_before = before
.lines()
.rev()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
if blank_lines_before != 0 {
checker.add_check(Check::new(
CheckKind::NoBlankLineBeforeFunction(blank_lines_before),
range_for(docstring),
));
}
}
if checker.settings.enabled.contains(&CheckCode::D202) {
let blank_lines_after = after
.lines()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
let all_blank_after = after
.lines()
.skip(1)
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
// Report a D202 violation if the docstring is followed by a blank line
// and the blank line is not itself followed by an inner function or
// class.
if !all_blank_after
&& blank_lines_after != 0
&& !(blank_lines_after == 1
&& INNER_FUNCTION_OR_CLASS_REGEX.is_match(after))
{
checker.add_check(Check::new(
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
range_for(docstring),
));
}
}
}
}
}
}
/// D203, D204, D211
pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = &definition.docstring {
if let DefinitionKind::Class(parent) | DefinitionKind::NestedClass(parent) =
&definition.kind
{
if let ExprKind::Constant {
value: Constant::Str(_),
..
} = &docstring.node
{
let (before, _, after) = checker
.locator
.partition_source_code_at(&Range::from_located(parent), &range_for(docstring));
if checker.settings.enabled.contains(&CheckCode::D203)
|| checker.settings.enabled.contains(&CheckCode::D211)
{
let blank_lines_before = before
.lines()
.rev()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
if blank_lines_before != 0
&& checker.settings.enabled.contains(&CheckCode::D211)
{
checker.add_check(Check::new(
CheckKind::NoBlankLineBeforeClass(blank_lines_before),
range_for(docstring),
));
}
if blank_lines_before != 1
&& checker.settings.enabled.contains(&CheckCode::D203)
{
checker.add_check(Check::new(
CheckKind::OneBlankLineBeforeClass(blank_lines_before),
range_for(docstring),
));
}
}
if checker.settings.enabled.contains(&CheckCode::D204) {
let blank_lines_after = after
.lines()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
let all_blank_after = after
.lines()
.skip(1)
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
if !all_blank_after && blank_lines_after != 1 {
checker.add_check(Check::new(
CheckKind::OneBlankLineAfterClass(blank_lines_after),
range_for(docstring),
));
}
}
}
}
}
}
/// D205
pub fn blank_after_summary(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let mut lines_count = 1;
let mut blanks_count = 0;
for line in string.trim().lines().skip(1) {
lines_count += 1;
if line.trim().is_empty() {
blanks_count += 1;
} else {
break;
}
}
if lines_count > 1 && blanks_count != 1 {
checker.add_check(Check::new(
CheckKind::NoBlankLineAfterSummary,
range_for(docstring),
));
}
}
}
}
/// D209
pub fn newline_after_last_paragraph(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let mut line_count = 0;
for line in string.lines() {
if !line.trim().is_empty() {
line_count += 1;
}
if line_count > 1 {
let content = checker
.locator
.slice_source_code_range(&range_for(docstring));
if let Some(line) = content.lines().last() {
let line = line.trim();
if line != "\"\"\"" && line != "'''" {
checker.add_check(Check::new(
CheckKind::NewLineAfterLastParagraph,
range_for(docstring),
));
}
}
return;
}
}
}
}
}
/// D210
pub fn no_surrounding_whitespace(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let mut lines = string.lines();
if let Some(line) = lines.next() {
if line.trim().is_empty() {
return;
}
if line.starts_with(' ') || (matches!(lines.next(), None) && line.ends_with(' ')) {
checker.add_check(Check::new(
CheckKind::NoSurroundingWhitespace,
range_for(docstring),
));
}
}
}
}
}
/// D212, D213
pub fn multi_line_summary_start(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if string.lines().nth(1).is_some() {
let content = checker
.locator
.slice_source_code_range(&range_for(docstring));
if let Some(first_line) = content.lines().next() {
let first_line = first_line.trim();
if first_line == "\"\"\"" || first_line == "'''" {
if checker.settings.enabled.contains(&CheckCode::D212) {
checker.add_check(Check::new(
CheckKind::MultiLineSummaryFirstLine,
range_for(docstring),
));
}
} else if checker.settings.enabled.contains(&CheckCode::D213) {
checker.add_check(Check::new(
CheckKind::MultiLineSummarySecondLine,
range_for(docstring),
));
}
}
}
}
}
}
/// D300
pub fn triple_quotes(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let content = checker
.locator
.slice_source_code_range(&range_for(docstring));
if string.contains("\"\"\"") {
if !content.starts_with("'''") {
checker.add_check(Check::new(
CheckKind::UsesTripleQuotes,
range_for(docstring),
));
}
} else if !content.starts_with("\"\"\"") {
checker.add_check(Check::new(
CheckKind::UsesTripleQuotes,
range_for(docstring),
));
}
}
}
}
/// D400
pub fn ends_with_period(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if let Some(string) = string.lines().next() {
if !string.ends_with('.') {
checker.add_check(Check::new(CheckKind::EndsInPeriod, range_for(docstring)));
}
}
}
}
}
/// D402
pub fn no_signature(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent)
| DefinitionKind::Method(parent) = definition.kind
{
if let StmtKind::FunctionDef { name, .. } = &parent.node {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if let Some(first_line) = string.lines().next() {
if first_line.contains(&format!("{name}(")) {
checker.add_check(Check::new(
CheckKind::NoSignature,
range_for(docstring),
));
}
}
}
}
}
}
}
/// D403
pub fn capitalized(checker: &mut Checker, definition: &Definition) {
if !matches!(definition.kind, DefinitionKind::Function(_)) {
return;
}
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if let Some(first_word) = string.split(' ').next() {
if first_word == first_word.to_uppercase() {
return;
}
for char in first_word.chars() {
if !char.is_ascii_alphabetic() && char != '\'' {
return;
}
}
if let Some(first_char) = first_word.chars().next() {
if !first_char.is_uppercase() {
checker.add_check(Check::new(
CheckKind::FirstLineCapitalized,
range_for(docstring),
));
}
}
}
}
}
}
/// D404
pub fn starts_with_this(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let trimmed = string.trim();
if trimmed.is_empty() {
return;
}
if let Some(first_word) = string.split(' ').next() {
if first_word
.replace(|c: char| !c.is_alphanumeric(), "")
.to_lowercase()
== "this"
{
checker.add_check(Check::new(CheckKind::NoThisPrefix, range_for(docstring)));
}
}
}
}
}
/// D415
pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if let Some(string) = string.lines().next() {
if !(string.ends_with('.') || string.ends_with('!') || string.ends_with('?')) {
checker.add_check(Check::new(
CheckKind::EndsInPunctuation,
range_for(docstring),
));
}
}
}
}
}
/// D418
pub fn if_needed(checker: &mut Checker, definition: &Definition) {
if definition.docstring.is_some() {
if let DefinitionKind::Function(stmt)
| DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) = definition.kind
{
if is_overload(stmt) {
checker.add_check(Check::new(
CheckKind::SkipDocstring,
Range::from_located(stmt),
));
}
}
}
}
/// D419
pub fn not_empty(checker: &mut Checker, definition: &Definition) -> bool {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if string.trim().is_empty() {
if checker.settings.enabled.contains(&CheckCode::D419) {
checker.add_check(Check::new(CheckKind::NonEmpty, range_for(docstring)));
}
return false;
}
}
}
true
}
pub fn check_sections(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let lines: Vec<&str> = string.lines().collect();
if lines.len() < 2 {
return;
}
for context in &section_contexts(&lines) {
check_numpy_section(checker, definition, context);
}
}
}
}

384
src/docstrings/sections.rs Normal file
View File

@@ -0,0 +1,384 @@
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
use titlecase::titlecase;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::docstring_checks::range_for;
use crate::docstrings::types::Definition;
static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"Short Summary",
"Extended Summary",
"Parameters",
"Returns",
"Yields",
"Other Parameters",
"Raises",
"See Also",
"Notes",
"References",
"Examples",
"Attributes",
"Methods",
])
});
static NUMPY_SECTION_NAMES_LOWERCASE: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"short summary",
"extended summary",
"parameters",
"returns",
"yields",
"other parameters",
"raises",
"see also",
"notes",
"references",
"examples",
"attributes",
"methods",
])
});
// TODO(charlie): Include Google section names.
// static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
// BTreeSet::from([
// "Args",
// "Arguments",
// "Attention",
// "Attributes",
// "Caution",
// "Danger",
// "Error",
// "Example",
// "Examples",
// "Hint",
// "Important",
// "Keyword Args",
// "Keyword Arguments",
// "Methods",
// "Note",
// "Notes",
// "Return",
// "Returns",
// "Raises",
// "References",
// "See Also",
// "Tip",
// "Todo",
// "Warning",
// "Warnings",
// "Warns",
// "Yield",
// "Yields",
// ])
// });
fn get_leading_words(line: &str) -> String {
line.trim()
.chars()
.take_while(|char| char.is_alphanumeric() || char.is_whitespace())
.collect()
}
fn suspected_as_section(line: &str) -> bool {
NUMPY_SECTION_NAMES_LOWERCASE.contains(&get_leading_words(line).to_lowercase().as_str())
}
#[derive(Debug)]
pub struct SectionContext<'a> {
section_name: String,
previous_line: &'a str,
line: &'a str,
following_lines: &'a [&'a str],
original_index: usize,
is_last_section: bool,
}
/// Check if the suspected context is really a section header.
fn is_docstring_section(context: &SectionContext) -> bool {
let section_name_suffix = context
.line
.trim()
.strip_prefix(&context.section_name)
.unwrap()
.trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
if !this_looks_like_a_section_name {
return false;
}
let prev_line = context.previous_line.trim();
let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
.into_iter()
.any(|char| prev_line.ends_with(char));
let prev_line_looks_like_end_of_paragraph =
prev_line_ends_with_punctuation || prev_line.is_empty();
if !prev_line_looks_like_end_of_paragraph {
return false;
}
true
}
/// Extract all `SectionContext` values from a docstring.
pub fn section_contexts<'a>(lines: &'a [&'a str]) -> Vec<SectionContext<'a>> {
let suspected_section_indices: Vec<usize> = lines
.iter()
.enumerate()
.filter_map(|(lineno, line)| {
if lineno > 0 && suspected_as_section(line) {
Some(lineno)
} else {
None
}
})
.collect();
let mut contexts = vec![];
for lineno in suspected_section_indices {
let context = SectionContext {
section_name: get_leading_words(lines[lineno]),
previous_line: lines[lineno - 1],
line: lines[lineno],
following_lines: &lines[lineno + 1..],
original_index: lineno,
is_last_section: false,
};
if is_docstring_section(&context) {
contexts.push(context);
}
}
let mut truncated_contexts = vec![];
let mut end: Option<usize> = None;
for context in contexts.into_iter().rev() {
let next_end = context.original_index;
truncated_contexts.push(SectionContext {
section_name: context.section_name,
previous_line: context.previous_line,
line: context.line,
following_lines: if let Some(end) = end {
&lines[context.original_index + 1..end]
} else {
context.following_lines
},
original_index: context.original_index,
is_last_section: end.is_none(),
});
end = Some(next_end);
}
truncated_contexts.reverse();
truncated_contexts
}
fn check_blanks_and_section_underline(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
let mut blank_lines_after_header = 0;
for line in context.following_lines {
if !line.trim().is_empty() {
break;
}
blank_lines_after_header += 1;
}
// Nothing but blank lines after the section header.
if blank_lines_after_header == context.following_lines.len() {
// D407
if checker.settings.enabled.contains(&CheckCode::D407) {
checker.add_check(Check::new(
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
range_for(docstring),
));
}
// D414
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
range_for(docstring),
));
}
return;
}
let non_empty_line = context.following_lines[blank_lines_after_header];
let dash_line_found = non_empty_line
.chars()
.all(|char| char.is_whitespace() || char == '-');
if !dash_line_found {
// D407
if checker.settings.enabled.contains(&CheckCode::D407) {
checker.add_check(Check::new(
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
range_for(docstring),
));
}
if blank_lines_after_header > 0 {
// D212
if checker.settings.enabled.contains(&CheckCode::D212) {
checker.add_check(Check::new(
CheckKind::NoBlankLinesBetweenHeaderAndContent(
context.section_name.to_string(),
),
range_for(docstring),
));
}
}
} else {
if blank_lines_after_header > 0 {
// D408
if checker.settings.enabled.contains(&CheckCode::D408) {
checker.add_check(Check::new(
CheckKind::SectionUnderlineAfterName(context.section_name.to_string()),
range_for(docstring),
));
}
}
if non_empty_line
.trim()
.chars()
.filter(|char| *char == '-')
.count()
!= context.section_name.len()
{
// D409
if checker.settings.enabled.contains(&CheckCode::D409) {
checker.add_check(Check::new(
CheckKind::SectionUnderlineMatchesSectionLength(
context.section_name.to_string(),
),
range_for(docstring),
));
}
}
// TODO(charlie): Implement D215, which requires indentation and leading space tracking.
let line_after_dashes_index = blank_lines_after_header + 1;
if line_after_dashes_index < context.following_lines.len() {
let line_after_dashes = context.following_lines[line_after_dashes_index];
if line_after_dashes.trim().is_empty() {
let rest_of_lines = &context.following_lines[line_after_dashes_index..];
if rest_of_lines.iter().all(|line| line.trim().is_empty()) {
// D414
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
range_for(docstring),
));
}
} else {
// 412
if checker.settings.enabled.contains(&CheckCode::D412) {
checker.add_check(Check::new(
CheckKind::NoBlankLinesBetweenHeaderAndContent(
context.section_name.to_string(),
),
range_for(docstring),
));
}
}
}
} else {
// D414
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
range_for(docstring),
));
}
}
}
}
fn check_common_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) {
// TODO(charlie): Implement D214, which requires indentation and leading space tracking.
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
if checker.settings.enabled.contains(&CheckCode::D405) {
if !NUMPY_SECTION_NAMES.contains(&context.section_name.as_str())
&& NUMPY_SECTION_NAMES.contains(titlecase(&context.section_name).as_str())
{
checker.add_check(Check::new(
CheckKind::CapitalizeSectionName(context.section_name.to_string()),
range_for(docstring),
))
}
}
if context
.following_lines
.last()
.map(|line| !line.trim().is_empty())
.unwrap_or(true)
{
if context.is_last_section {
if checker.settings.enabled.contains(&CheckCode::D413) {
checker.add_check(Check::new(
CheckKind::BlankLineAfterLastSection(context.section_name.to_string()),
range_for(docstring),
))
}
} else {
if checker.settings.enabled.contains(&CheckCode::D410) {
checker.add_check(Check::new(
CheckKind::BlankLineAfterSection(context.section_name.to_string()),
range_for(docstring),
))
}
}
}
if checker.settings.enabled.contains(&CheckCode::D411) {
if !context.previous_line.is_empty() {
checker.add_check(Check::new(
CheckKind::BlankLineBeforeSection(context.section_name.to_string()),
range_for(docstring),
))
}
}
}
pub fn check_numpy_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
// TODO(charlie): Implement `_check_parameters_section`.
check_common_section(checker, definition, context);
check_blanks_and_section_underline(checker, definition, context);
if checker.settings.enabled.contains(&CheckCode::D406) {
let suffix = context
.line
.trim()
.strip_prefix(&context.section_name)
.unwrap();
if !suffix.is_empty() {
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
checker.add_check(Check::new(
CheckKind::NewLineAfterSectionName(context.section_name.to_string()),
range_for(docstring),
))
}
}
}

23
src/docstrings/types.rs Normal file
View File

@@ -0,0 +1,23 @@
use rustpython_ast::{Expr, Stmt};
#[derive(Debug)]
pub enum DefinitionKind<'a> {
Module,
Package,
Class(&'a Stmt),
NestedClass(&'a Stmt),
Function(&'a Stmt),
NestedFunction(&'a Stmt),
Method(&'a Stmt),
}
#[derive(Debug)]
pub struct Definition<'a> {
pub kind: DefinitionKind<'a>,
pub docstring: Option<&'a Expr>,
}
pub enum Documentable {
Class,
Function,
}

View File

@@ -1,3 +1,4 @@
#![allow(clippy::collapsible_if, clippy::collapsible_else_if)]
use std::path::Path;
use anyhow::Result;
@@ -15,16 +16,20 @@ pub mod cache;
pub mod check_ast;
mod check_lines;
pub mod checks;
pub mod cli;
pub mod code_gen;
mod docstrings;
pub mod fs;
pub mod linter;
pub mod logging;
pub mod message;
mod noqa;
mod plugins;
pub mod printer;
pub mod pyproject;
mod python;
pub mod settings;
pub mod visibility;
/// Run ruff over Python source code directly.
pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
@@ -40,7 +45,7 @@ pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings = Settings::from_raw(RawSettings::from_pyproject(pyproject, project_root)?);
let settings = Settings::from_raw(RawSettings::from_pyproject(&pyproject, &project_root)?);
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);

View File

@@ -83,6 +83,36 @@ pub(crate) fn check_path(
Ok(checks)
}
pub fn lint_stdin(path: &Path, stdin: &str, settings: &Settings) -> Result<Vec<Message>> {
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(stdin);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Generate checks.
let checks = check_path(
path,
stdin,
tokens,
&noqa_line_for,
settings,
&fixer::Mode::None,
)?;
// Convert to messages.
Ok(checks
.into_iter()
.map(|check| Message {
kind: check.kind,
fixed: check.fix.map(|fix| fix.applied).unwrap_or_default(),
location: check.location,
end_location: check.end_location,
filename: path.to_string_lossy().to_string(),
})
.collect())
}
pub fn lint_path(
path: &Path,
settings: &Settings,
@@ -340,9 +370,93 @@ mod tests {
}
#[test]
fn f401() -> Result<()> {
fn w292_0() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F401.py"),
Path::new("./resources/test/fixtures/W292_0.py"),
&settings::Settings::for_rule(CheckCode::W292),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn w292_1() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/W292_1.py"),
&settings::Settings::for_rule(CheckCode::W292),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn w292_2() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/W292_2.py"),
&settings::Settings::for_rule(CheckCode::W292),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f401_0() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F401_0.py"),
&settings::Settings::for_rule(CheckCode::F401),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f401_1() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F401_1.py"),
&settings::Settings::for_rule(CheckCode::F401),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f401_2() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F401_2.py"),
&settings::Settings::for_rule(CheckCode::F401),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f401_3() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F401_3.py"),
&settings::Settings::for_rule(CheckCode::F401),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f401_4() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F401_4.py"),
&settings::Settings::for_rule(CheckCode::F401),
&fixer::Mode::Generate,
)?;
@@ -678,66 +792,6 @@ mod tests {
Ok(())
}
#[test]
fn m001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/M001.py"),
&settings::Settings::for_rules(vec![CheckCode::M001, CheckCode::E501, CheckCode::F841]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn r001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/R001.py"),
&settings::Settings::for_rule(CheckCode::R001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn r002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/R002.py"),
&settings::Settings::for_rule(CheckCode::R002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn init() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/__init__.py"),
&settings::Settings::for_rules(vec![CheckCode::F821, CheckCode::F822]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn future_annotations() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/future_annotations.py"),
&settings::Settings::for_rules(vec![CheckCode::F401, CheckCode::F821]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e999() -> Result<()> {
let mut checks = check_path(
@@ -787,10 +841,610 @@ mod tests {
}
#[test]
fn spr001() -> Result<()> {
fn b011() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/SPR001.py"),
&settings::Settings::for_rule(CheckCode::SPR001),
Path::new("./resources/test/fixtures/B011.py"),
&settings::Settings::for_rule(CheckCode::B011),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn b014() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/B014.py"),
&settings::Settings::for_rule(CheckCode::B014),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn b025() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/B025.py"),
&settings::Settings::for_rule(CheckCode::B025),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c400() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C400.py"),
&settings::Settings::for_rule(CheckCode::C400),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c401() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C401.py"),
&settings::Settings::for_rule(CheckCode::C401),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C402.py"),
&settings::Settings::for_rule(CheckCode::C402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c403() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C403.py"),
&settings::Settings::for_rule(CheckCode::C403),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c404() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C404.py"),
&settings::Settings::for_rule(CheckCode::C404),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c405() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C405.py"),
&settings::Settings::for_rule(CheckCode::C405),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c406() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C406.py"),
&settings::Settings::for_rule(CheckCode::C406),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c408() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C408.py"),
&settings::Settings::for_rule(CheckCode::C408),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c409() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C409.py"),
&settings::Settings::for_rule(CheckCode::C409),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c410() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C410.py"),
&settings::Settings::for_rule(CheckCode::C410),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c414() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C414.py"),
&settings::Settings::for_rule(CheckCode::C414),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c415() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C415.py"),
&settings::Settings::for_rule(CheckCode::C415),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d100() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D100),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d101() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D101),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d102() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D102),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d103() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D103),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d104() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D104),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d105() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D105),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d106() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D106),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d107() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D107),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d201() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D201),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d202() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D202),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d203() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D203),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d204() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D204),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d205() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D205),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d209() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D209),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d210() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D210),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d211() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D211),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d212() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D212),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d213() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D213),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d300() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D300),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d400() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D400),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d403() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D403),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d404() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D404),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d405() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D405),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d406() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D406),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d407() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D407),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d408() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D408),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d409() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D409),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d410() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D410),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d411() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D411),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d412() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D412),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d413() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D413),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d414() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D414),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d415() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D415),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d418() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D418),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d419() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D419),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -833,4 +1487,124 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U002.py"),
&settings::Settings::for_rule(CheckCode::U002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u003() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U003.py"),
&settings::Settings::for_rule(CheckCode::U003),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u004() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U004.py"),
&settings::Settings::for_rule(CheckCode::U004),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u005() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U005.py"),
&settings::Settings::for_rule(CheckCode::U005),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u006() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U006.py"),
&settings::Settings::for_rule(CheckCode::U006),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u007() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U007.py"),
&settings::Settings::for_rule(CheckCode::U007),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u008() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U008.py"),
&settings::Settings::for_rule(CheckCode::U008),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn m001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/M001.py"),
&settings::Settings::for_rules(vec![CheckCode::M001, CheckCode::E501, CheckCode::F841]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn init() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/__init__.py"),
&settings::Settings::for_rules(vec![CheckCode::F821, CheckCode::F822]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn future_annotations() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/future_annotations.py"),
&settings::Settings::for_rules(vec![CheckCode::F401, CheckCode::F821]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -1,102 +1,37 @@
use std::io;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::Instant;
use anyhow::Result;
use clap::{command, Parser};
use clap::Parser;
use colored::Colorize;
use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use regex::Regex;
use walkdir::DirEntry;
use ::ruff::cache;
use ::ruff::checks::CheckCode;
use ::ruff::checks::CheckKind;
use ::ruff::fs::iter_python_files;
use ::ruff::linter::add_noqa_to_path;
use ::ruff::linter::lint_path;
use ::ruff::logging::set_up_logging;
use ::ruff::message::Message;
use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::pyproject::{self, StrCheckCodePair};
use ::ruff::settings::CurrentSettings;
use ::ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ::ruff::tell_user;
use ruff::cache;
use ruff::checks::CheckCode;
use ruff::checks::CheckKind;
use ruff::cli::{warn_on, Cli, Warnable};
use ruff::fs::iter_python_files;
use ruff::linter::add_noqa_to_path;
use ruff::linter::autoformat_path;
use ruff::linter::{lint_path, lint_stdin};
use ruff::logging::set_up_logging;
use ruff::message::Message;
use ruff::printer::{Printer, SerializationFormat};
use ruff::pyproject::{self};
use ruff::settings::CurrentSettings;
use ruff::settings::RawSettings;
use ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ruff::tell_user;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Parser)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
#[command(version)]
struct Cli {
#[arg(required = true)]
files: Vec<PathBuf>,
/// Enable verbose logging.
#[arg(short, long)]
verbose: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[arg(short, long)]
quiet: bool,
/// Exit with status code "0", even upon detecting errors.
#[arg(short, long)]
exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
watch: bool,
/// Attempt to automatically fix lint errors.
#[arg(short, long)]
fix: bool,
/// Disable cache reads.
#[arg(short, long)]
no_cache: bool,
/// List of error codes to enable.
#[arg(long, value_delimiter = ',')]
select: Vec<CheckCode>,
/// Like --select, but adds additional error codes on top of the selected ones.
#[arg(long, value_delimiter = ',')]
extend_select: Vec<CheckCode>,
/// List of error codes to ignore.
#[arg(long, value_delimiter = ',')]
ignore: Vec<CheckCode>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[arg(long, value_delimiter = ',')]
extend_ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[arg(long, value_delimiter = ',')]
exclude: Vec<String>,
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[arg(long, value_delimiter = ',')]
extend_exclude: Vec<String>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
per_file_ignores: Vec<StrCheckCodePair>,
/// Output serialization format for error messages.
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
format: SerializationFormat,
/// See the files ruff will be run against with the current settings.
#[arg(long)]
show_files: bool,
/// See ruff's settings.
#[arg(long)]
show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
#[arg(long)]
add_noqa: bool,
/// Enable automatic formatting.
#[arg(long)]
autoformat: bool,
/// Regular expression matching the name of dummy variables.
#[arg(long)]
dummy_variable_rgx: Option<Regex>,
}
#[cfg(feature = "update-informer")]
fn check_for_updates() {
use update_informer::{registry, Check};
@@ -121,8 +56,11 @@ fn check_for_updates() {
}
}
fn show_settings(settings: RawSettings) {
println!("{:#?}", CurrentSettings::from_settings(settings));
fn show_settings(settings: RawSettings, project_root: Option<PathBuf>, pyproject: Option<PathBuf>) {
println!(
"{:#?}",
CurrentSettings::from_settings(settings, project_root, pyproject)
);
}
fn show_files(files: &[PathBuf], settings: &Settings) {
@@ -137,6 +75,19 @@ fn show_files(files: &[PathBuf], settings: &Settings) {
}
}
fn read_from_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
Ok(buffer)
}
fn run_once_stdin(settings: &Settings, filename: &Path) -> Result<Vec<Message>> {
let stdin = read_from_stdin()?;
let mut messages = lint_stdin(filename, &stdin, settings)?;
messages.sort_unstable();
Ok(messages)
}
fn run_once(
files: &[PathBuf],
settings: &Settings,
@@ -288,7 +239,7 @@ fn inner_main() -> Result<ExitCode> {
.map(|pair| PerFileIgnore::new(pair, &project_root))
.collect();
let mut settings = RawSettings::from_pyproject(pyproject, project_root)?;
let mut settings = RawSettings::from_pyproject(&pyproject, &project_root)?;
if !exclude.is_empty() {
settings.exclude = exclude;
}
@@ -299,9 +250,25 @@ fn inner_main() -> Result<ExitCode> {
settings.per_file_ignores = per_file_ignores;
}
if !cli.select.is_empty() {
warn_on(
Warnable::Select,
&cli.select,
&cli.ignore,
&cli.extend_ignore,
&settings,
&pyproject,
);
settings.select = cli.select;
}
if !cli.extend_select.is_empty() {
warn_on(
Warnable::ExtendSelect,
&cli.extend_select,
&cli.ignore,
&cli.extend_ignore,
&settings,
&pyproject,
);
settings.extend_select = cli.extend_select;
}
if !cli.ignore.is_empty() {
@@ -310,6 +277,9 @@ fn inner_main() -> Result<ExitCode> {
if !cli.extend_ignore.is_empty() {
settings.extend_ignore = cli.extend_ignore;
}
if let Some(target_version) = cli.target_version {
settings.target_version = target_version;
}
if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx {
settings.dummy_variable_rgx = dummy_variable_rgx;
}
@@ -319,7 +289,7 @@ fn inner_main() -> Result<ExitCode> {
return Ok(ExitCode::FAILURE);
}
if cli.show_settings {
show_settings(settings);
show_settings(settings, project_root, pyproject);
return Ok(ExitCode::SUCCESS);
}
@@ -395,7 +365,17 @@ fn inner_main() -> Result<ExitCode> {
println!("Formatted {modifications} files.");
}
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
let messages = if cli.files == vec![PathBuf::from("-")] {
if cli.fix {
eprintln!("Warning: --fix is not enabled when reading from stdin.");
}
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
let path = Path::new(&filename);
run_once_stdin(&settings, path)?
} else {
run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?
};
if !cli.quiet {
printer.write_once(&messages)?;
}
@@ -403,7 +383,7 @@ fn inner_main() -> Result<ExitCode> {
#[cfg(feature = "update-informer")]
check_for_updates();
if !messages.is_empty() && !cli.exit_zero {
if messages.iter().any(|message| !message.fixed) && !cli.exit_zero {
return Ok(ExitCode::FAILURE);
}
}

View File

@@ -45,7 +45,7 @@ impl fmt::Display for Message {
":".cyan(),
self.location.column(),
":".cyan(),
self.kind.code().as_str().red().bold(),
self.kind.code().as_ref().red().bold(),
self.kind.body()
)
}

View File

@@ -104,9 +104,7 @@ fn add_noqa_inner(
.unwrap_or(lineno);
if !codes.is_empty() {
let matches = matches_by_line
.entry(noqa_lineno)
.or_insert_with(BTreeSet::new);
let matches = matches_by_line.entry(noqa_lineno).or_default();
matches.append(&mut codes);
}
}
@@ -127,7 +125,7 @@ fn add_noqa_inner(
Directive::All(start, _) => output.push_str(&line[..start]),
Directive::Codes(start, _, _) => output.push_str(&line[..start]),
};
let codes: Vec<&str> = codes.iter().map(|code| code.as_str()).collect();
let codes: Vec<&str> = codes.iter().map(|code| code.as_ref()).collect();
output.push_str(" # noqa: ");
output.push_str(&codes.join(", "));
output.push('\n');
@@ -152,12 +150,12 @@ pub fn add_noqa(
#[cfg(test)]
mod tests {
use crate::ast::types::Range;
use anyhow::Result;
use rustpython_parser::ast::Location;
use rustpython_parser::lexer;
use rustpython_parser::lexer::LexResult;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::noqa::{add_noqa_inner, extract_noqa_line_for};

29
src/plugins.rs Normal file
View File

@@ -0,0 +1,29 @@
pub use assert_false::assert_false;
pub use assert_tuple::assert_tuple;
pub use deprecated_unittest_alias::deprecated_unittest_alias;
pub use duplicate_exceptions::duplicate_exceptions;
pub use if_tuple::if_tuple;
pub use invalid_print_syntax::invalid_print_syntax;
pub use print_call::print_call;
pub use super_call_with_parameters::super_call_with_parameters;
pub use type_of_primitive::type_of_primitive;
pub use unnecessary_abspath::unnecessary_abspath;
pub use use_pep585_annotation::use_pep585_annotation;
pub use use_pep604_annotation::use_pep604_annotation;
pub use useless_metaclass_type::useless_metaclass_type;
pub use useless_object_inheritance::useless_object_inheritance;
mod assert_false;
mod assert_tuple;
mod deprecated_unittest_alias;
mod duplicate_exceptions;
mod if_tuple;
mod invalid_print_syntax;
mod print_call;
mod super_call_with_parameters;
mod type_of_primitive;
mod unnecessary_abspath;
mod use_pep585_annotation;
mod use_pep604_annotation;
mod useless_metaclass_type;
mod useless_object_inheritance;

View File

@@ -0,0 +1,61 @@
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind, Fix};
use crate::code_gen::SourceGenerator;
fn assertion_error(msg: &Option<Box<Expr>>) -> Stmt {
Stmt::new(
Default::default(),
Default::default(),
StmtKind::Raise {
exc: Some(Box::new(Expr::new(
Default::default(),
Default::default(),
ExprKind::Call {
func: Box::new(Expr::new(
Default::default(),
Default::default(),
ExprKind::Name {
id: "AssertionError".to_string(),
ctx: ExprContext::Load,
},
)),
args: if let Some(msg) = msg {
vec![*msg.clone()]
} else {
vec![]
},
keywords: vec![],
},
))),
cause: None,
},
)
}
pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: &Option<Box<Expr>>) {
if let ExprKind::Constant {
value: Constant::Bool(false),
..
} = &test.node
{
let mut check = Check::new(CheckKind::DoNotAssertFalse, Range::from_located(test));
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_stmt(&assertion_error(msg)) {
if let Ok(content) = generator.generate() {
check.amend(Fix {
content,
location: stmt.location,
end_location: stmt.end_location,
applied: false,
})
}
}
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,13 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::checkers;
use crate::ast::types::{CheckLocator, Range};
use crate::check_ast::Checker;
pub fn assert_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
if let Some(check) =
checkers::assert_tuple(test, checker.locate_check(Range::from_located(stmt)))
{
checker.add_check(check);
}
}

View File

@@ -0,0 +1,56 @@
use std::collections::BTreeMap;
use once_cell::sync::Lazy;
use rustpython_ast::{Expr, ExprKind, Location};
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind, Fix};
static DEPRECATED_ALIASES: Lazy<BTreeMap<&'static str, &'static str>> = Lazy::new(|| {
BTreeMap::from([
("failUnlessEqual", "assertEqual"),
("assertEquals", "assertEqual"),
("failIfEqual", "assertNotEqual"),
("assertNotEquals", "assertNotEqual"),
("failUnless", "assertTrue"),
("assert_", "assertTrue"),
("failIf", "assertFalse"),
("failUnlessRaises", "assertRaises"),
("failUnlessAlmostEqual", "assertAlmostEqual"),
("assertAlmostEquals", "assertAlmostEqual"),
("failIfAlmostEqual", "assertNotAlmostEqual"),
("assertNotAlmostEquals", "assertNotAlmostEqual"),
("assertRegexpMatches", "assertRegex"),
("assertNotRegexpMatches", "assertNotRegex"),
("assertRaisesRegexp", "assertRaisesRegex"),
])
});
pub fn deprecated_unittest_alias(checker: &mut Checker, expr: &Expr) {
if let ExprKind::Attribute { value, attr, .. } = &expr.node {
if let Some(target) = DEPRECATED_ALIASES.get(attr.as_str()) {
if let ExprKind::Name { id, .. } = &value.node {
if id == "self" {
let mut check = Check::new(
CheckKind::DeprecatedUnittestAlias(attr.to_string(), target.to_string()),
Range::from_located(expr),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: format!("self.{}", target),
location: Location::new(expr.location.row(), expr.location.column()),
end_location: Location::new(
expr.end_location.row(),
expr.end_location.column(),
),
applied: false,
});
}
checker.add_check(check);
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Stmt};
use crate::ast::helpers;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::code_gen::SourceGenerator;
fn type_pattern(elts: Vec<&Expr>) -> Expr {
Expr::new(
Default::default(),
Default::default(),
ExprKind::Tuple {
elts: elts.into_iter().cloned().collect(),
ctx: ExprContext::Load,
},
)
}
pub fn duplicate_handler_exceptions(
checker: &mut Checker,
expr: &Expr,
elts: &Vec<Expr>,
) -> BTreeSet<String> {
let mut seen: BTreeSet<String> = Default::default();
let mut duplicates: BTreeSet<String> = Default::default();
let mut unique_elts: Vec<&Expr> = Default::default();
for type_ in elts {
if let Some(name) = helpers::compose_call_path(type_) {
if seen.contains(&name) {
duplicates.insert(name);
} else {
seen.insert(name);
unique_elts.push(type_);
}
}
}
if checker.settings.enabled.contains(&CheckCode::B014) {
// TODO(charlie): Handle "BaseException" and redundant exception aliases.
if !duplicates.is_empty() {
let mut check = Check::new(
CheckKind::DuplicateHandlerException(
duplicates.into_iter().sorted().collect::<Vec<String>>(),
),
checker.locate_check(Range::from_located(expr)),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
// TODO(charlie): If we have a single element, remove the tuple.
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_expr(&type_pattern(unique_elts), 0) {
if let Ok(content) = generator.generate() {
check.amend(Fix {
content,
location: expr.location,
end_location: expr.end_location,
applied: false,
})
}
}
}
checker.add_check(check);
}
}
seen
}
pub fn duplicate_exceptions(checker: &mut Checker, stmt: &Stmt, handlers: &[Excepthandler]) {
let mut seen: BTreeSet<String> = Default::default();
let mut duplicates: BTreeSet<String> = Default::default();
for handler in handlers {
match &handler.node {
ExcepthandlerKind::ExceptHandler { type_, .. } => {
if let Some(type_) = type_ {
match &type_.node {
ExprKind::Attribute { .. } | ExprKind::Name { .. } => {
if let Some(name) = helpers::compose_call_path(type_) {
if seen.contains(&name) {
duplicates.insert(name);
} else {
seen.insert(name);
}
}
}
ExprKind::Tuple { elts, .. } => {
for name in duplicate_handler_exceptions(checker, type_, elts) {
if seen.contains(&name) {
duplicates.insert(name);
} else {
seen.insert(name);
}
}
}
_ => {}
}
}
}
}
}
if checker.settings.enabled.contains(&CheckCode::B025) {
for duplicate in duplicates.into_iter().sorted() {
checker.add_check(Check::new(
CheckKind::DuplicateTryBlockException(duplicate),
checker.locate_check(Range::from_located(stmt)),
));
}
}
}

11
src/plugins/if_tuple.rs Normal file
View File

@@ -0,0 +1,11 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::checkers;
use crate::ast::types::{CheckLocator, Range};
use crate::check_ast::Checker;
pub fn if_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
if let Some(check) = checkers::if_tuple(test, checker.locate_check(Range::from_located(stmt))) {
checker.add_check(check);
}
}

View File

@@ -0,0 +1,23 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::{Binding, BindingKind, Range};
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
pub fn invalid_print_syntax(checker: &mut Checker, left: &Expr) {
if let ExprKind::Name { id, .. } = &left.node {
if id == "print" {
let scope = checker.current_scope();
if let Some(Binding {
kind: BindingKind::Builtin,
..
}) = scope.values.get("print")
{
checker.add_check(Check::new(
CheckKind::InvalidPrintSyntax,
Range::from_located(left),
));
}
}
}
}

46
src/plugins/print_call.rs Normal file
View File

@@ -0,0 +1,46 @@
use log::error;
use rustpython_ast::{Expr, Stmt, StmtKind};
use crate::ast::checkers;
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
use crate::checks::CheckCode;
pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
if let Some(mut check) = checkers::print_call(
expr,
func,
checker.settings.enabled.contains(&CheckCode::T201),
checker.settings.enabled.contains(&CheckCode::T203),
) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let context = checker.binding_context();
if matches!(
checker.parents[context.defined_by].node,
StmtKind::Expr { .. }
) {
let deleted: Vec<&Stmt> = checker
.deletions
.iter()
.map(|index| checker.parents[*index])
.collect();
match fixes::remove_stmt(
checker.parents[context.defined_by],
context.defined_in.map(|index| checker.parents[index]),
&deleted,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
checker.deletions.insert(context.defined_by);
}
check.amend(fix)
}
Err(e) => error!("Failed to fix unused imports: {}", e),
}
}
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,31 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::{checkers, helpers};
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
pub fn super_call_with_parameters(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) {
// Only bother going through the super check at all if we're in a `super` call.
// (We check this in `check_super_args` too, so this is just an optimization.)
if helpers::is_super_call_with_arguments(func, args) {
let scope = checker.current_scope();
let parents: Vec<&Stmt> = checker
.parent_stack
.iter()
.map(|index| checker.parents[*index])
.collect();
if let Some(mut check) = checkers::super_args(scope, &parents, expr, func, args) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_super_arguments(&mut checker.locator, expr) {
check.amend(fix);
}
}
checker.add_check(check)
}
}
}

View File

@@ -0,0 +1,25 @@
use rustpython_ast::Expr;
use crate::ast::checkers;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{CheckKind, Fix};
pub fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, args: &Vec<Expr>) {
if let Some(mut check) =
checkers::type_of_primitive(func, args, checker.locate_check(Range::from_located(expr)))
{
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let CheckKind::TypeOfPrimitive(primitive) = &check.kind {
check.amend(Fix {
content: primitive.builtin(),
location: expr.location,
end_location: expr.end_location,
applied: false,
});
}
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,23 @@
use rustpython_ast::Expr;
use crate::ast::checkers;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::Fix;
pub fn unnecessary_abspath(checker: &mut Checker, expr: &Expr, func: &Expr, args: &Vec<Expr>) {
if let Some(mut check) =
checkers::unnecessary_abspath(func, args, checker.locate_check(Range::from_located(expr)))
{
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "__file__".to_string(),
location: expr.location,
end_location: expr.end_location,
applied: false,
});
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,26 @@
use rustpython_ast::Expr;
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind, Fix};
use crate::python::typing;
pub fn use_pep585_annotation(checker: &mut Checker, expr: &Expr, id: &str) {
// TODO(charlie): Verify that the builtin is imported from the `typing` module.
if typing::is_pep585_builtin(id) {
let mut check = Check::new(
CheckKind::UsePEP585Annotation(id.to_string()),
Range::from_located(expr),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: id.to_lowercase(),
location: expr.location,
end_location: expr.end_location,
applied: false,
})
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,100 @@
use rustpython_ast::{Constant, Expr, ExprKind, Operator};
use crate::ast::helpers::match_name_or_attr;
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind, Fix};
use crate::code_gen::SourceGenerator;
fn optional(expr: &Expr) -> Expr {
Expr::new(
Default::default(),
Default::default(),
ExprKind::BinOp {
left: Box::new(expr.clone()),
op: Operator::BitOr,
right: Box::new(Expr::new(
Default::default(),
Default::default(),
ExprKind::Constant {
value: Constant::None,
kind: None,
},
)),
},
)
}
fn union(elts: &[Expr]) -> Expr {
if elts.len() == 1 {
elts[0].clone()
} else {
Expr::new(
Default::default(),
Default::default(),
ExprKind::BinOp {
left: Box::new(union(&elts[..elts.len() - 1])),
op: Operator::BitOr,
right: Box::new(elts[elts.len() - 1].clone()),
},
)
}
}
pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, slice: &Expr) {
if match_name_or_attr(value, "Optional") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_expr(&optional(slice), 0) {
if let Ok(content) = generator.generate() {
check.amend(Fix {
content,
location: expr.location,
end_location: expr.end_location,
applied: false,
})
}
}
}
checker.add_check(check);
} else if match_name_or_attr(value, "Union") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
match &slice.node {
ExprKind::Slice { .. } => {
// Invalid type annotation.
}
ExprKind::Tuple { elts, .. } => {
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_expr(&union(elts), 0) {
if let Ok(content) = generator.generate() {
check.amend(Fix {
content,
location: expr.location,
end_location: expr.end_location,
applied: false,
})
}
}
}
_ => {
// Single argument.
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_expr(slice, 0) {
if let Ok(content) = generator.generate() {
check.amend(Fix {
content,
location: expr.location,
end_location: expr.end_location,
applied: false,
});
}
}
}
}
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,44 @@
use log::error;
use rustpython_ast::{Expr, Stmt};
use crate::ast::checkers;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
pub fn useless_metaclass_type(
checker: &mut Checker,
stmt: &Stmt,
value: &Expr,
targets: &Vec<Expr>,
) {
if let Some(mut check) = checkers::useless_metaclass_type(
targets,
value,
checker.locate_check(Range::from_located(stmt)),
) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let context = checker.binding_context();
let deleted: Vec<&Stmt> = checker
.deletions
.iter()
.map(|index| checker.parents[*index])
.collect();
match fixes::remove_stmt(
checker.parents[context.defined_by],
context.defined_in.map(|index| checker.parents[index]),
&deleted,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
checker.deletions.insert(context.defined_by);
}
check.amend(fix)
}
Err(e) => error!("Failed to fix unused imports: {}", e),
}
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,29 @@
use rustpython_ast::{Expr, Keyword, Stmt};
use crate::ast::checkers;
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
pub fn useless_object_inheritance(
checker: &mut Checker,
stmt: &Stmt,
name: &str,
bases: &[Expr],
keywords: &[Keyword],
) {
let scope = checker.current_scope();
if let Some(mut check) = checkers::useless_object_inheritance(name, bases, scope) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_class_def_base(
&mut checker.locator,
&stmt.location,
check.location,
bases,
keywords,
) {
check.amend(fix);
}
}
checker.add_check(check);
}
}

View File

@@ -9,6 +9,7 @@ use serde::{Deserialize, Deserializer};
use crate::checks::CheckCode;
use crate::fs;
use crate::settings::PythonVersion;
pub fn load_config(pyproject: &Option<PathBuf>) -> Result<Config> {
match pyproject {
@@ -41,6 +42,7 @@ pub struct Config {
#[serde(default)]
pub per_file_ignores: Vec<StrCheckCodePair>,
pub dummy_variable_rgx: Option<String>,
pub target_version: Option<PythonVersion>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -152,12 +154,13 @@ mod tests {
use anyhow::Result;
use super::StrCheckCodePair;
use crate::checks::CheckCode;
use crate::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Config, PyProject, Tools,
};
use super::StrCheckCodePair;
#[test]
fn deserialize() -> Result<()> {
let pyproject: PyProject = toml::from_str(r#""#)?;
@@ -189,6 +192,7 @@ mod tests {
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
})
})
);
@@ -213,6 +217,7 @@ line-length = 79
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
})
})
);
@@ -237,6 +242,7 @@ exclude = ["foo.py"]
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
})
})
);
@@ -261,6 +267,7 @@ select = ["E501"]
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
})
})
);
@@ -286,6 +293,7 @@ ignore = ["E501"]
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
})
})
);
@@ -354,6 +362,7 @@ other-attribute = 1
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
}
);

View File

@@ -7,6 +7,7 @@ static ANNOTATED_SUBSCRIPTS: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
"AbstractAsyncContextManager",
"AbstractContextManager",
"AbstractSet",
// "Annotated",
"AsyncContextManager",
"AsyncGenerator",
"AsyncIterable",
@@ -87,3 +88,14 @@ static ANNOTATED_SUBSCRIPTS: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
pub fn is_annotated_subscript(name: &str) -> bool {
ANNOTATED_SUBSCRIPTS.contains(name)
}
pub fn is_pep593_annotated_subscript(name: &str) -> bool {
name == "Annotated"
}
static PEP_585_BUILTINS: Lazy<BTreeSet<&'static str>> =
Lazy::new(|| BTreeSet::from(["Dict", "FrozenSet", "List", "Set", "Tuple", "Type"]));
pub fn is_pep585_builtin(name: &str) -> bool {
PEP_585_BUILTINS.contains(name)
}

View File

@@ -1,16 +1,50 @@
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use glob::Pattern;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::checks::{CheckCode, DEFAULT_CHECK_CODES};
use crate::fs;
use crate::pyproject::{load_config, StrCheckCodePair};
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize)]
pub enum PythonVersion {
Py33,
Py34,
Py35,
Py36,
Py37,
Py38,
Py39,
Py310,
Py311,
}
impl FromStr for PythonVersion {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"py33" => Ok(PythonVersion::Py33),
"py34" => Ok(PythonVersion::Py34),
"py35" => Ok(PythonVersion::Py35),
"py36" => Ok(PythonVersion::Py36),
"py37" => Ok(PythonVersion::Py37),
"py38" => Ok(PythonVersion::Py38),
"py39" => Ok(PythonVersion::Py39),
"py310" => Ok(PythonVersion::Py310),
"py311" => Ok(PythonVersion::Py311),
_ => Err(anyhow!("Unknown version: {}", string)),
}
}
}
#[derive(Debug, Clone, Hash)]
pub enum FilePattern {
Simple(&'static str),
@@ -60,9 +94,8 @@ pub struct RawSettings {
pub ignore: Vec<CheckCode>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub project_root: Option<PathBuf>,
pub pyproject: Option<PathBuf>,
pub select: Vec<CheckCode>,
pub target_version: PythonVersion,
}
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
@@ -94,44 +127,43 @@ static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
impl RawSettings {
pub fn from_pyproject(
pyproject: Option<PathBuf>,
project_root: Option<PathBuf>,
pyproject: &Option<PathBuf>,
project_root: &Option<PathBuf>,
) -> Result<Self> {
let config = load_config(&pyproject)?;
let config = load_config(pyproject)?;
Ok(RawSettings {
dummy_variable_rgx: match config.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
target_version: config.target_version.unwrap_or(PythonVersion::Py310),
exclude: config
.exclude
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.map(|path| FilePattern::from_user(path, project_root))
.collect()
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
extend_exclude: config
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.map(|path| FilePattern::from_user(path, project_root))
.collect(),
extend_ignore: config.extend_ignore,
select: config
.select
.unwrap_or_else(|| DEFAULT_CHECK_CODES.to_vec()),
extend_select: config.extend_select,
ignore: config.ignore,
line_length: config.line_length.unwrap_or(88),
per_file_ignores: config
.per_file_ignores
.into_iter()
.map(|pair| PerFileIgnore::new(pair, &project_root))
.map(|pair| PerFileIgnore::new(pair, project_root))
.collect(),
project_root,
pyproject,
select: config
.select
.unwrap_or_else(|| DEFAULT_CHECK_CODES.to_vec()),
})
}
}
@@ -144,6 +176,7 @@ pub struct Settings {
pub extend_exclude: Vec<FilePattern>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub target_version: PythonVersion,
}
impl Settings {
@@ -165,6 +198,7 @@ impl Settings {
extend_exclude: settings.extend_exclude,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
target_version: PythonVersion::Py310,
}
}
@@ -176,6 +210,7 @@ impl Settings {
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
}
}
@@ -187,6 +222,7 @@ impl Settings {
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
}
}
}
@@ -238,13 +274,18 @@ pub struct CurrentSettings {
pub ignore: Vec<CheckCode>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCode>,
pub target_version: PythonVersion,
pub project_root: Option<PathBuf>,
pub pyproject: Option<PathBuf>,
pub select: Vec<CheckCode>,
}
impl CurrentSettings {
pub fn from_settings(settings: RawSettings) -> Self {
pub fn from_settings(
settings: RawSettings,
project_root: Option<PathBuf>,
pyproject: Option<PathBuf>,
) -> Self {
Self {
dummy_variable_rgx: settings.dummy_variable_rgx,
exclude: settings
@@ -262,9 +303,10 @@ impl CurrentSettings {
ignore: settings.ignore,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
project_root: settings.project_root,
pyproject: settings.pyproject,
select: settings.select,
target_version: settings.target_version,
project_root,
pyproject,
}
}
}

View File

@@ -0,0 +1,37 @@
---
source: src/linter.rs
expression: checks
---
- kind: DoNotAssertFalse
location:
row: 8
column: 8
end_location:
row: 8
column: 13
fix:
content: raise AssertionError()
location:
row: 8
column: 1
end_location:
row: 8
column: 13
applied: false
- kind: DoNotAssertFalse
location:
row: 10
column: 8
end_location:
row: 10
column: 13
fix:
content: "raise AssertionError(\"message\")"
location:
row: 10
column: 1
end_location:
row: 10
column: 24
applied: false

View File

@@ -0,0 +1,59 @@
---
source: src/linter.rs
expression: checks
---
- kind:
DuplicateHandlerException:
- OSError
location:
row: 17
column: 9
end_location:
row: 17
column: 25
fix:
content: "OSError,"
location:
row: 17
column: 9
end_location:
row: 17
column: 25
applied: false
- kind:
DuplicateHandlerException:
- MyError
location:
row: 28
column: 9
end_location:
row: 28
column: 25
fix:
content: "MyError,"
location:
row: 28
column: 9
end_location:
row: 28
column: 25
applied: false
- kind:
DuplicateHandlerException:
- re.error
location:
row: 49
column: 9
end_location:
row: 49
column: 27
fix:
content: "re.error,"
location:
row: 49
column: 9
end_location:
row: 49
column: 27
applied: false

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
DuplicateTryBlockException: ValueError
location:
row: 15
column: 1
end_location:
row: 22
column: 1
fix: ~
- kind:
DuplicateTryBlockException: pickle.PickleError
location:
row: 22
column: 1
end_location:
row: 31
column: 1
fix: ~
- kind:
DuplicateTryBlockException: TypeError
location:
row: 31
column: 1
end_location:
row: 39
column: 1
fix: ~
- kind:
DuplicateTryBlockException: ValueError
location:
row: 31
column: 1
end_location:
row: 39
column: 1
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: UnnecessaryGeneratorList
location:
row: 1
column: 5
end_location:
row: 1
column: 30
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: UnnecessaryGeneratorList
location:
row: 1
column: 5
end_location:
row: 1
column: 29
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: UnnecessaryListComprehensionDict
location:
row: 1
column: 5
end_location:
row: 1
column: 35
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: UnnecessaryListComprehensionSet
location:
row: 1
column: 5
end_location:
row: 1
column: 31
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: UnnecessaryListComprehensionDict
location:
row: 1
column: 5
end_location:
row: 1
column: 37
fix: ~

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessaryLiteralSet: list
location:
row: 1
column: 6
end_location:
row: 1
column: 17
fix: ~
- kind:
UnnecessaryLiteralSet: tuple
location:
row: 2
column: 6
end_location:
row: 2
column: 17
fix: ~
- kind:
UnnecessaryLiteralSet: list
location:
row: 3
column: 6
end_location:
row: 3
column: 13
fix: ~
- kind:
UnnecessaryLiteralSet: tuple
location:
row: 4
column: 6
end_location:
row: 4
column: 13
fix: ~

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessaryLiteralDict: list
location:
row: 1
column: 6
end_location:
row: 1
column: 20
fix: ~
- kind:
UnnecessaryLiteralDict: tuple
location:
row: 2
column: 6
end_location:
row: 2
column: 21
fix: ~
- kind:
UnnecessaryLiteralDict: list
location:
row: 3
column: 6
end_location:
row: 3
column: 14
fix: ~
- kind:
UnnecessaryLiteralDict: tuple
location:
row: 4
column: 6
end_location:
row: 4
column: 14
fix: ~

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessaryCollectionCall: tuple
location:
row: 1
column: 5
end_location:
row: 1
column: 12
fix: ~
- kind:
UnnecessaryCollectionCall: list
location:
row: 2
column: 5
end_location:
row: 2
column: 11
fix: ~
- kind:
UnnecessaryCollectionCall: dict
location:
row: 3
column: 6
end_location:
row: 3
column: 12
fix: ~
- kind:
UnnecessaryCollectionCall: dict
location:
row: 4
column: 6
end_location:
row: 4
column: 15
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessaryLiteralWithinTupleCall: list
location:
row: 1
column: 6
end_location:
row: 1
column: 19
fix: ~
- kind:
UnnecessaryLiteralWithinTupleCall: tuple
location:
row: 2
column: 6
end_location:
row: 2
column: 19
fix: ~
- kind:
UnnecessaryLiteralWithinTupleCall: list
location:
row: 3
column: 6
end_location:
row: 3
column: 15
fix: ~

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessaryLiteralWithinListCall: list
location:
row: 1
column: 6
end_location:
row: 1
column: 18
fix: ~
- kind:
UnnecessaryLiteralWithinListCall: tuple
location:
row: 2
column: 6
end_location:
row: 2
column: 18
fix: ~
- kind:
UnnecessaryLiteralWithinListCall: list
location:
row: 3
column: 6
end_location:
row: 3
column: 14
fix: ~
- kind:
UnnecessaryLiteralWithinListCall: tuple
location:
row: 4
column: 6
end_location:
row: 4
column: 14
fix: ~

View File

@@ -0,0 +1,148 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessaryDoubleCastOrProcess:
- list
- list
location:
row: 2
column: 1
end_location:
row: 2
column: 14
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- tuple
- list
location:
row: 3
column: 1
end_location:
row: 3
column: 15
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- list
- tuple
location:
row: 4
column: 1
end_location:
row: 4
column: 15
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- tuple
- tuple
location:
row: 5
column: 1
end_location:
row: 5
column: 16
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- set
- set
location:
row: 6
column: 1
end_location:
row: 6
column: 12
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- list
- set
location:
row: 7
column: 1
end_location:
row: 7
column: 13
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- tuple
- set
location:
row: 8
column: 1
end_location:
row: 8
column: 14
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- sorted
- set
location:
row: 9
column: 1
end_location:
row: 9
column: 15
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- reversed
- set
location:
row: 10
column: 1
end_location:
row: 10
column: 17
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- list
- sorted
location:
row: 11
column: 1
end_location:
row: 11
column: 16
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- tuple
- sorted
location:
row: 12
column: 1
end_location:
row: 12
column: 17
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- sorted
- sorted
location:
row: 13
column: 1
end_location:
row: 13
column: 18
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- reversed
- sorted
location:
row: 14
column: 1
end_location:
row: 14
column: 20
fix: ~

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessarySubscriptReversal: set
location:
row: 2
column: 5
end_location:
row: 2
column: 19
fix: ~
- kind:
UnnecessarySubscriptReversal: reversed
location:
row: 3
column: 5
end_location:
row: 3
column: 24
fix: ~
- kind:
UnnecessarySubscriptReversal: sorted
location:
row: 4
column: 5
end_location:
row: 4
column: 22
fix: ~
- kind:
UnnecessarySubscriptReversal: sorted
location:
row: 5
column: 5
end_location:
row: 5
column: 36
fix: ~

View File

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

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: PublicClass
location:
row: 14
column: 1
end_location:
row: 67
column: 1
fix: ~

View File

@@ -0,0 +1,29 @@
---
source: src/linter.rs
expression: checks
---
- kind: PublicMethod
location:
row: 22
column: 5
end_location:
row: 25
column: 5
fix: ~
- kind: PublicMethod
location:
row: 51
column: 5
end_location:
row: 54
column: 5
fix: ~
- kind: PublicMethod
location:
row: 63
column: 5
end_location:
row: 67
column: 1
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: PublicFunction
location:
row: 395
column: 1
end_location:
row: 396
column: 1
fix: ~

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