Compare commits

..

85 Commits

Author SHA1 Message Date
Charlie Marsh
062c41b6f5 Bump version to 0.0.93 2022-10-31 09:20:39 -04:00
Charlie Marsh
78889efa37 Modify public API to return Check rather than Message (#524) 2022-10-31 09:20:14 -04:00
Charlie Marsh
97fc281779 Remove erroneous foo.py file 2022-10-30 19:19:49 -04:00
Charlie Marsh
138b06c98a Bump version to 0.0.92 2022-10-30 18:04:30 -04:00
Charlie Marsh
2415d73260 Remove RustPython fork (#523) 2022-10-30 18:04:05 -04:00
Charlie Marsh
b060ae2f22 Move SourceCodeLocator to its own module (#522) 2022-10-30 15:51:59 -04:00
Charlie Marsh
9aa91d3d3c Add a cargo bench for SourceCodeLocator (#521) 2022-10-30 13:50:42 -04:00
Charlie Marsh
dcedb801e5 Tweak a few check messages (#520) 2022-10-30 13:13:37 -04:00
Charlie Marsh
d3e7fdabb5 Implement consistent newline handling for SourceCodeLocator (#519) 2022-10-30 13:11:08 -04:00
Charlie Marsh
dfa5a4f0f7 Fix 9/32 flake8-bugbear reference 2022-10-30 13:03:39 -04:00
Charlie Marsh
58a2d600da Avoid flagging D202 for inner functions and classes (#518) 2022-10-30 13:02:55 -04:00
Harutaka Kawamura
7ecbfe4f6a Implement B006 (#515) 2022-10-30 12:57:57 -04:00
Charlie Marsh
d8248104a7 Avoid re-indenting empty lines in D207 (#517) 2022-10-30 09:41:54 -04:00
Charlie Marsh
6eb09122c0 Add final newline in SourceCodeLocator 2022-10-29 19:23:16 -04:00
Charlie Marsh
f84c1f1fa1 Bump version to 0.0.91 2022-10-29 18:49:26 -04:00
Charlie Marsh
3aa9528229 Avoid flake8-comprehensions errors for dicts with kwargs (#512) 2022-10-29 18:49:06 -04:00
Charlie Marsh
5a3f06bab1 Bump version to 0.0.90 2022-10-29 18:34:38 -04:00
Charlie Marsh
db59d5b558 Use a single SourceCodeLocator everywhere (#510) 2022-10-29 18:23:24 -04:00
Charlie Marsh
2fcbf3ab62 Simplify SourceCodeLocator offset computation (#509) 2022-10-29 18:13:42 -04:00
Anders Kaseorg
fa9b10be72 Remove leading space from C416 message (#508) 2022-10-29 17:40:50 -04:00
Charlie Marsh
c495cef529 Move pyproject.toml logging to debug (#506) 2022-10-29 17:07:46 -04:00
Charlie Marsh
c0c8dff6ce Implement configuration options for pep8-naming (#505) 2022-10-29 17:00:30 -04:00
Charlie Marsh
80b00cc89f Add error code categories to table of contents (#504) 2022-10-29 16:39:55 -04:00
Charlie Marsh
934db3d179 Bump version to 0.0.89 2022-10-29 15:39:17 -04:00
Charlie Marsh
6a040a0405 Update checks_gen.rs 2022-10-29 15:39:02 -04:00
Harutaka Kawamura
2821ef0f69 Implement B013 (#503) 2022-10-29 15:36:29 -04:00
Harutaka Kawamura
343d931ddb Ignore unittest methods and functions in N802 (#502) 2022-10-29 15:36:09 -04:00
Harutaka Kawamura
3fc257f71b Implement N806, 815, 816, 818 (#501) 2022-10-29 15:35:56 -04:00
Charlie Marsh
6dbb0a17e9 Update README to reflect selection groups 2022-10-28 19:15:46 -04:00
Charlie Marsh
ae5ad6a4ac Bump version to 0.0.88 2022-10-28 19:11:04 -04:00
Charlie Marsh
549af6c584 Regenerate CheckCodePrefix 2022-10-28 19:10:45 -04:00
Charlie Marsh
9a799eb4e6 Bump version to 0.0.87 2022-10-28 19:00:03 -04:00
Anders Kaseorg
f260b873b6 Fix “not a char boundary” error with Unicode in extract_quote (#497) 2022-10-28 18:59:12 -04:00
Charlie Marsh
782a90b584 Add tests for resolve_codes (#498) 2022-10-28 18:58:46 -04:00
Charlie Marsh
7df903dc4d Move around and rename some of the Settings structs (#496) 2022-10-28 18:46:54 -04:00
Charlie Marsh
8fc5e91ec7 Enable prefix-based check code selection (#493) 2022-10-28 18:19:57 -04:00
Charlie Marsh
9ca1a2c273 Fix failing pyproject.toml test 2022-10-28 18:13:07 -04:00
Charlie Marsh
86265c1d7c Implement the flake8-quotes plugin (#495) 2022-10-28 17:52:11 -04:00
Charlie Marsh
a057c9a323 Move invalid_escape_sequence into pycodestyle (#494) 2022-10-28 12:20:11 -04:00
Trevor Gross
2e63bb6dcb Update hook id in README and in .pre-commit-config.yaml (#492) 2022-10-27 17:32:52 -04:00
Charlie Marsh
1b5db80b32 Update pre-commit invocation in README.md 2022-10-27 17:19:22 -04:00
Charlie Marsh
3f20cea402 Bump version to 0.0.86 2022-10-27 13:09:57 -04:00
Charlie Marsh
389fe1ff64 Avoid auto-fixing unused imports in __init__.py (#489) 2022-10-27 13:08:04 -04:00
Charlie Marsh
bad2d7ba85 Add example of per-file ignores to the README (#488) 2022-10-27 12:58:52 -04:00
Charlie Marsh
416aa298ac Allow whitespace in per-file ignore patterns (#487) 2022-10-27 12:55:28 -04:00
Charlie Marsh
a535b1adbf Replace compliance comments with check codes (#485) 2022-10-27 09:32:18 -04:00
Charlie Marsh
05fbd1a283 Bump version to 0.0.85 2022-10-26 19:13:04 -04:00
Charlie Marsh
63552cbc8e Implement W605 (invalid escape sequence) (#482) 2022-10-26 19:10:24 -04:00
Charlie Marsh
c00bd489f1 Fix multi-segment import removal (#480) 2022-10-26 16:43:55 -04:00
Charlie Marsh
16c2e3a995 Handle multi-segment import-from removal (#479) 2022-10-26 16:36:12 -04:00
Anders Kaseorg
650b025181 Suppress “No pyproject.toml found” message with --quiet (#478) 2022-10-26 16:03:25 -04:00
Anders Kaseorg
8fe46f7400 Rename --quiet to --silent and make --quiet only log errors (#477) 2022-10-26 16:03:10 -04:00
Charlie Marsh
a9bcc15797 Bump version to 0.0.84 2022-10-26 12:01:55 -04:00
Charlie Marsh
b6c856bd07 Implement B007 (unused loop control variable) (#473) 2022-10-26 12:01:27 -04:00
Charlie Marsh
4beea0484a Use lazy initialization for SourceCodeLocator (#472) 2022-10-26 11:27:48 -04:00
Suguru Yamamoto
2679db1d10 Correct EOL offset for lines ending with multi-byte char (#471) 2022-10-26 11:00:27 -04:00
Charlie Marsh
3e73462e04 Optimize imports 2022-10-25 22:06:51 -04:00
Charlie Marsh
f63a87737a DRY up usages of matches with fixer Mode (#470) 2022-10-25 22:02:58 -04:00
Charlie Marsh
e7472eac1c Increment flake8-bugbear to 8/32 2022-10-25 21:54:00 -04:00
Charlie Marsh
db3c847771 Bump version to 0.0.83 2022-10-25 21:24:13 -04:00
Charlie Marsh
4adbfc24a5 Increment flake8-bugbear to 6/32 2022-10-25 21:23:13 -04:00
Charlie Marsh
8f734a6562 Implement B002 (unary prefix increment) (#468) 2022-10-25 21:10:51 -04:00
Charlie Marsh
bcf7519eb3 Implement B017 (no assertRaises(Exception)) (#467) 2022-10-25 20:55:00 -04:00
Heyward Fann
66089052ee chore: typo on #283 link (#464) 2022-10-25 08:02:11 -04:00
Jeong YunWon
d50cc8ff65 Restyle flake8_comprehensions::check to reduce indent (#462) 2022-10-24 15:16:56 -04:00
Harutaka Kawamura
b75ea94f58 Fix uppercase and lowercase check (#461) 2022-10-22 12:49:13 -04:00
Harutaka Kawamura
2c24e2fd28 Enable N811, 812, 813, 814, 817 for Import (#460) 2022-10-22 08:31:02 -04:00
Charlie Marsh
f8dc208665 Tweak messages for pep8-naming rules 2022-10-21 17:06:16 -04:00
Charlie Marsh
c72b8e8d1e Bump version to 0.0.82 2022-10-21 12:15:19 -04:00
Harutaka Kawamura
b108c693fa Implement N811, 812, 813, 814, and 817 (#457) 2022-10-20 12:20:44 -04:00
Harutaka Kawamura
aac1912ea7 Implement N807 (#456) 2022-10-20 11:12:02 -04:00
Charlie Marsh
3dcd26aac3 Add SectionUnderlineAfterName to autofix list 2022-10-17 22:08:49 -04:00
Charlie Marsh
e53b9807f6 Bump version to 0.0.81 2022-10-17 21:43:49 -04:00
Charlie Marsh
36fe8b76d4 Enable autofix for over- and under-indented docstrings (#451) 2022-10-17 21:43:38 -04:00
Charlie Marsh
f832f88c75 Implement autofix support for D214, D405, D406, and D416 (#450) 2022-10-17 17:37:20 -04:00
Charlie Marsh
659a28de02 Bump version to 0.0.80 2022-10-17 17:02:44 -04:00
Charlie Marsh
583149a472 Break up autofix/fixes.rs (#449) 2022-10-17 17:01:50 -04:00
Charlie Marsh
206e6463be Implement autofix for more docstring-related rules (#448) 2022-10-17 16:56:47 -04:00
Charlie Marsh
118a9feec8 Split checks and plugins into source-related modules (#447) 2022-10-17 15:38:49 -04:00
Charlie Marsh
1f2ccb059a Break up some nested if statements 2022-10-17 13:08:33 -04:00
Charlie Marsh
f4d9d6c858 Fix typo in prev_visible_scope 2022-10-17 13:03:50 -04:00
Charlie Marsh
3477f5664a Fix README link to near-parity 2022-10-17 11:57:07 -04:00
Charlie Marsh
edefa5219c Update RustPython to get main versions of end_location etc. (#445) 2022-10-17 11:52:40 -04:00
Charlie Marsh
cf0d198365 Bump version to 0.0.79 2022-10-16 21:39:01 -04:00
Charlie Marsh
08b14ed77e Remove leading 'or' from fixable match 2022-10-16 21:38:22 -04:00
304 changed files with 12447 additions and 6500 deletions

View File

@@ -1,8 +1,8 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.40
rev: v0.0.93
hooks:
- id: lint
- id: ruff
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.10.1

179
Cargo.lock generated
View File

@@ -37,6 +37,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "annotate-snippets"
version = "0.6.1"
@@ -368,6 +374,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.0.73"
@@ -416,6 +428,45 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "ciborium"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369"
[[package]]
name = "ciborium-ll"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "3.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
dependencies = [
"bitflags",
"clap_lex 0.2.4",
"indexmap",
"textwrap 0.16.0",
]
[[package]]
name = "clap"
version = "4.0.15"
@@ -425,7 +476,7 @@ dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"clap_lex 0.3.0",
"once_cell",
"strsim",
"termcolor",
@@ -444,6 +495,15 @@ dependencies = [
"syn",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "clap_lex"
version = "0.3.0"
@@ -466,6 +526,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "codegen"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff61280aed771c3070e7dcc9e050c66f1eb1e3b96431ba66f9f74641d02fc41d"
dependencies = [
"indexmap",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
@@ -539,6 +608,42 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "criterion"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
dependencies = [
"anes",
"atty",
"cast",
"ciborium",
"clap 3.2.23",
"criterion-plot",
"itertools",
"lazy_static",
"num-traits",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.6"
@@ -1032,6 +1137,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "half"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1562,6 +1673,12 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "oorandom"
version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "opaque-debug"
version = "0.2.3"
@@ -1770,6 +1887,34 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "plotters"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142"
[[package]]
name = "plotters-svg"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f"
dependencies = [
"plotters-backend",
]
[[package]]
name = "polling"
version = "2.3.0"
@@ -2045,17 +2190,19 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.78"
version = "0.0.93"
dependencies = [
"anyhow",
"assert_cmd",
"bincode",
"cacache",
"chrono",
"clap",
"clap 4.0.15",
"clearscreen",
"codegen",
"colored",
"common-path",
"criterion",
"dirs 4.0.0",
"fern",
"filetime",
@@ -2079,7 +2226,7 @@ dependencies = [
"strum",
"strum_macros",
"test-case",
"textwrap",
"textwrap 0.15.1",
"titlecase",
"toml",
"update-informer",
@@ -2101,7 +2248,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=210db77e4274787028dc3ebc0b4841d1334c8c10#210db77e4274787028dc3ebc0b4841d1334c8c10"
source = "git+https://github.com/RustPython/RustPython.git?rev=77b821a1941019fe34f73ce17cea013ae1b98fd0#77b821a1941019fe34f73ce17cea013ae1b98fd0"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -2111,7 +2258,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=210db77e4274787028dc3ebc0b4841d1334c8c10#210db77e4274787028dc3ebc0b4841d1334c8c10"
source = "git+https://github.com/RustPython/RustPython.git?rev=77b821a1941019fe34f73ce17cea013ae1b98fd0#77b821a1941019fe34f73ce17cea013ae1b98fd0"
dependencies = [
"ascii",
"cfg-if 1.0.0",
@@ -2134,7 +2281,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=210db77e4274787028dc3ebc0b4841d1334c8c10#210db77e4274787028dc3ebc0b4841d1334c8c10"
source = "git+https://github.com/RustPython/RustPython.git?rev=77b821a1941019fe34f73ce17cea013ae1b98fd0#77b821a1941019fe34f73ce17cea013ae1b98fd0"
dependencies = [
"bincode",
"bitflags",
@@ -2151,7 +2298,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=210db77e4274787028dc3ebc0b4841d1334c8c10#210db77e4274787028dc3ebc0b4841d1334c8c10"
source = "git+https://github.com/RustPython/RustPython.git?rev=77b821a1941019fe34f73ce17cea013ae1b98fd0#77b821a1941019fe34f73ce17cea013ae1b98fd0"
dependencies = [
"ahash",
"anyhow",
@@ -2539,6 +2686,12 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
[[package]]
name = "thiserror"
version = "1.0.37"
@@ -2579,6 +2732,16 @@ dependencies = [
"crunchy",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "tinyvec"
version = "1.6.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.78"
version = "0.0.93"
edition = "2021"
[lib]
@@ -26,9 +26,9 @@ once_cell = { version = "1.13.1" }
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/charliermarsh/RustPython.git", rev = "210db77e4274787028dc3ebc0b4841d1334c8c10" }
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "210db77e4274787028dc3ebc0b4841d1334c8c10" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "210db77e4274787028dc3ebc0b4841d1334c8c10" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "77b821a1941019fe34f73ce17cea013ae1b98fd0" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "77b821a1941019fe34f73ce17cea013ae1b98fd0" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "77b821a1941019fe34f73ce17cea013ae1b98fd0" }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" }
strum = { version = "0.24.1", features = ["strum_macros"] }
@@ -50,6 +50,8 @@ getrandom = { version = "0.2.7", features = ["js"] }
[dev-dependencies]
assert_cmd = { version = "2.0.4" }
codegen = { version = "0.2.0" }
criterion = { version = "0.4.0" }
insta = { version = "1.19.1", features = ["yaml"] }
test-case = { version = "2.2.2" }
@@ -68,3 +70,7 @@ opt-level = 3
[profile.dev.package.similar]
opt-level = 3
[[bench]]
name = "source_code_locator"
harness = false

243
README.md
View File

@@ -22,7 +22,7 @@ An extremely fast Python linter, written in Rust.
- 📦 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired cache support
- 🔧 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix)-inspired autofix support (e.g., automatically remove unused imports)
- 👀 [TypeScript](https://www.typescriptlang.org/docs/handbook/configuring-watch.html)-inspired `--watch` support, for continuous file monitoring
- ⚖️ [Near-parity](#Parity-with-Flake8) with the built-in Flake8 rule set
- ⚖️ [Near-parity](#how-does-ruff-compare-to-flake8) with the built-in Flake8 rule set
- 🔌 Native re-implementations of popular Flake8 plugins, like [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) ([`pydocstyle`](https://pypi.org/project/pydocstyle/))
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
@@ -38,13 +38,25 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
1. [Installation and Usage](#installation-and-usage)
2. [Configuration](#configuration)
3. [Supported Rules](#supported-rules)
4. [Editor Integrations](#editor-integrations)
5. [FAQ](#faq)
6. [Development](#development)
7. [Releases](#releases)
8. [Benchmarks](#benchmarks)
9. [License](#license)
10. [Contributing](#contributing)
1. [Pyflakes](#pyflakes)
2. [pycodestyle (error)](#pycodestyle-error)
3. [pycodestyle (warning)](#pycodestyle-warning)
4. [pydocstyle](#pydocstyle)
5. [pyupgrade](#pyupgrade)
6. [pep8-naming](#pep8-naming)
7. [flake8-comprehensions](#flake8-comprehensions)
8. [flake8-bugbear](#flake8-bugbear)
9. [flake8-builtins](#flake8-builtins)
10. [flake8-print](#flake8-print)
11. [flake8-quotes](#flake8-quotes)
12. [Meta rules](#meta-rules)
5. [Editor Integrations](#editor-integrations)
6. [FAQ](#faq)
7. [Development](#development)
8. [Releases](#releases)
9. [Benchmarks](#benchmarks)
10. [License](#license)
11. [Contributing](#contributing)
## Installation and Usage
@@ -77,11 +89,14 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.78
rev: v0.0.93
hooks:
- id: lint
- id: ruff
```
<!-- TODO(charlie): Remove this message a few versions after v0.0.86. -->
_Note: prior to `v0.0.86`, `ruff-pre-commit` used `lint` (rather than `ruff`) as the hook ID._
## Configuration
Ruff is configurable both via `pyproject.toml` and the command line.
@@ -91,13 +106,25 @@ For example, you could configure Ruff to only enforce a subset of rules with:
```toml
[tool.ruff]
line-length = 88
select = [
"F401",
"F403",
select = ["E", "F"]
ignore = ["E501"]
per-file-ignores = [
"__init__.py:F401",
"path/to/file.py:F401"
]
```
Alternatively, on the command-line:
Plugin configurations should be expressed as subsections, e.g.:
```toml
[tool.ruff]
line-length = 88
[tool.ruff.flake8-quotes]
docstring-quotes = "double"
```
Alternatively, common configuration settings can be provided via the command-line:
```shell
ruff path/to/code/ --select F401 --select F403
@@ -219,8 +246,7 @@ add `noqa` directives to all failing lines, with the appropriate error codes.**
## Supported Rules
By default, Ruff enables all `E`, `W`, and `F` error codes, which correspond to those built-in to
Flake8.
By default, Ruff enables all `E` and `F` error codes, which correspond to those built-in to Flake8.
The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` command-line option.
@@ -246,7 +272,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| F634 | IfTuple | If test is a tuple, which is always `True` | |
| F701 | BreakOutsideLoop | `break` outside loop | |
| F702 | ContinueOutsideLoop | `continue` not properly in loop | |
| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function/method | |
| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function | |
| F706 | ReturnOutsideFunction | `return` statement outside of a function/method | |
| F707 | DefaultExceptNotLast | An `except:` block as not the last exception handler | |
| F722 | ForwardAnnotationSyntaxError | Syntax error in forward annotation: `...` | |
@@ -257,7 +283,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| F841 | UnusedVariable | Local variable `...` is assigned to but never used | |
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | |
### pycodestyle
### pycodestyle (error)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -275,7 +301,13 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | |
| E902 | IOError | IOError: `...` | |
| E999 | SyntaxError | SyntaxError: `...` | |
### pycodestyle (warning)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| W292 | NoNewLineAtEndOfFile | No newline at end of file | |
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | |
### pydocstyle
@@ -294,36 +326,36 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | 🛠 |
| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring | 🛠 |
| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | 🛠 |
| D205 | NoBlankLineAfterSummary | 1 blank line required between summary line and description | 🛠 |
| D205 | BlankLineAfterSummary | 1 blank line required between summary line and description | 🛠 |
| D206 | IndentWithSpaces | Docstring should be indented with spaces, not tabs | |
| D207 | NoUnderIndentation | Docstring is under-indented | |
| D208 | NoOverIndentation | Docstring is over-indented | |
| D207 | NoUnderIndentation | Docstring is under-indented | 🛠 |
| D208 | NoOverIndentation | Docstring is over-indented | 🛠 |
| D209 | NewLineAfterLastParagraph | Multi-line docstring closing quotes should be on a separate line | 🛠 |
| D210 | NoSurroundingWhitespace | No whitespaces allowed surrounding docstring text | 🛠 |
| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | 🛠 |
| D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line | |
| D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | |
| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | |
| D215 | SectionUnderlineNotOverIndented | Section underline is over-indented ("Returns") | |
| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | 🛠 |
| D215 | SectionUnderlineNotOverIndented | Section underline is over-indented ("Returns") | 🛠 |
| 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' | |
| 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` | |
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("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") | |
| D404 | NoThisPrefix | First word of the docstring should not be 'This' | |
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("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") | 🛠 |
| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | 🛠 |
| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | |
| D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") | |
| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | 🛠 |
| D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") | 🛠 |
| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | 🛠 |
| D414 | NonEmptySection | Section has no content ("Returns") | |
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | |
| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | |
| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | 🛠 |
| D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` | |
| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | |
| D418 | SkipDocstring | Function decorated with `@overload` shouldn't contain a docstring | |
| D419 | NonEmpty | Docstring is empty | |
### pyupgrade
@@ -348,6 +380,16 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| N803 | InvalidArgumentName | Argument name `...` should be lowercase | |
| N804 | InvalidFirstArgumentNameForClassMethod | First argument of a class method should be named `cls` | |
| N805 | InvalidFirstArgumentNameForMethod | First argument of a method should be named `self` | |
| N806 | NonLowercaseVariableInFunction | Variable `...` in function should be lowercase | |
| N807 | DunderFunctionName | Function name should not start and end with `__` | |
| N811 | ConstantImportedAsNonConstant | Constant `...` imported as non-constant `...` | |
| N812 | LowercaseImportedAsNonLowercase | Lowercase `...` imported as non-lowercase `...` | |
| N813 | CamelcaseImportedAsLowercase | Camelcase `...` imported as lowercase `...` | |
| N814 | CamelcaseImportedAsConstant | Camelcase `...` imported as constant `...` | |
| N815 | MixedCaseVariableInClassScope | Variable `mixedCase` in class scope should not be mixedCase | |
| N816 | MixedCaseVariableInGlobalScope | Variable `mixedCase` in global scope should not be mixedCase | |
| N817 | CamelcaseImportedAsAcronym | Camelcase `...` imported as acronym `...` | |
| N818 | ErrorSuffixOnExceptionName | Exception name `...` should be named with an Error suffix | |
### flake8-comprehensions
@@ -367,15 +409,20 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| C413 | UnnecessaryCallAroundSorted | Unnecessary `(list\|reversed)` call around `sorted()` | |
| 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)()` | |
| C416 | UnnecessaryComprehension | Unnecessary `(list\|set)` comprehension (rewrite using `(list\|set)()`) | |
| C416 | UnnecessaryComprehension | Unnecessary `(list\|set)` comprehension (rewrite using `(list\|set)()`) | |
| C417 | UnnecessaryMap | Unnecessary `map` usage (rewrite using a `(list\|set\|dict)` comprehension) | |
### flake8-bugbear
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| B002 | UnaryPrefixIncrement | Python does not support the unary prefix increment. | |
| B006 | MutableArgumentDefault | Do not use mutable data structures for argument defaults. | |
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body. | 🛠 |
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError:` instead of `except (ValueError,):`. | |
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 |
| B017 | NoAssertRaisesException | `assertRaises(Exception):` should be considered evil. | |
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
### flake8-builtins
@@ -393,6 +440,15 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| T201 | PrintFound | `print` found | 🛠 |
| T203 | PPrintFound | `pprint` found | 🛠 |
### flake8-quotes
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| Q000 | BadQuotesInlineString | Single quotes found but double quotes preferred | |
| Q001 | BadQuotesMultilineString | Single quote multiline found but double quotes preferred | |
| Q002 | BadQuotesDocstring | Single quote docstring found but double quotes preferred | |
| Q003 | AvoidQuoteEscape | Change outer quotes to avoid escaping inner quotes | |
### Meta rules
| Code | Name | Message | Fix |
@@ -459,13 +515,15 @@ Ruff re-implements some of the most popular Flake8 plugins and related code qual
including:
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`yesqa`](https://github.com/asottile/yesqa)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (10/32)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
@@ -480,12 +538,14 @@ Beyond rule-set parity, Ruff suffers from the following limitations vis-à-vis F
Today, Ruff can be used to replace Flake8 when used with any of the following plugins:
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (10/32)
Ruff also implements the functionality that you get from [`yesqa`](https://github.com/asottile/yesqa),
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34).
@@ -506,7 +566,7 @@ on Rust at all.
### Can I write my own plugins for Ruff?
Ruff does not yet support third-party plugins, though a plugin system is within-scope for the
project. See [#283](https://github.com/charliermarsh/ruff/issues/2830) for more.
project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
### Does Ruff support NumPy- or Google-style docstrings?
@@ -514,35 +574,55 @@ Yes! To enable a specific docstring convention, start by enabling all `pydocstyl
then selectively disabling based on your [preferred convention](https://www.pydocstyle.org/en/latest/error_codes.html#default-conventions).
For example, if you're coming from `flake8-docstrings`, the following configuration is equivalent to
`--docstring-convention numpy`:
`--docstring-convention=numpy`:
```toml
[tool.ruff]
extend-select = [
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D200",
"D201",
"D202",
extend-select = ["D"]
extend-ignore = [
"D107",
"D203",
"D212",
"D213",
"D402",
"D413",
"D415",
"D416",
"D417",
]
```
Similarly, the following is equivalent to `--docstring-convention=google`:
```toml
[tool.ruff]
extend-select = ["D"]
extend-ignore = [
"D203",
"D204",
"D205",
"D206",
"D207",
"D208",
"D209",
"D210",
"D211",
"D213",
"D215",
"D400",
"D404",
"D406",
"D407",
"D408",
"D409",
"D413",
]
```
Similarly, the following is equivalent to `--docstring-convention=pep8`:
```toml
[tool.ruff]
extend-select = ["D"]
extend-ignore = [
"D203",
"D212",
"D213",
"D214",
"D215",
"D300",
"D400",
"D402",
"D403",
"D404",
"D405",
"D406",
@@ -551,51 +631,10 @@ extend-select = [
"D409",
"D410",
"D411",
"D412",
"D413",
"D418",
"D419",
]
```
Similarly, the following is equivalent to `--docstring-convention google`:
```toml
[tool.ruff]
extend-select = [
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
"D200",
"D201",
"D202",
"D205",
"D206",
"D207",
"D208",
"D209",
"D210",
"D211",
"D212",
"D214",
"D300",
"D402",
"D403",
"D405",
"D410",
"D411",
"D412",
"D414",
"D415",
"D416",
"D417",
"D418",
"D419",
]
```

View File

@@ -0,0 +1,16 @@
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use ruff::fs;
use ruff::source_code_locator::compute_offsets;
fn criterion_benchmark(c: &mut Criterion) {
let contents = fs::read_file(Path::new("resources/test/fixtures/D.py")).unwrap();
c.bench_function("compute_offsets", |b| {
b.iter(|| compute_offsets(black_box(&contents)))
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -0,0 +1,105 @@
//! Generate the CheckCodePrefix enum.
use std::collections::{BTreeMap, BTreeSet};
use codegen::{Scope, Type, Variant};
use itertools::Itertools;
use strum::IntoEnumIterator;
use ruff::checks::CheckCode;
fn main() {
// Build up a map from prefix to matching CheckCodes.
let mut prefix_to_codes: BTreeMap<String, BTreeSet<CheckCode>> = Default::default();
for check_code in CheckCode::iter() {
let as_ref = check_code.as_ref().to_string();
for i in 1..=as_ref.len() {
let prefix = as_ref[..i].to_string();
let entry = prefix_to_codes
.entry(prefix)
.or_insert_with(|| Default::default());
entry.insert(check_code.clone());
}
}
let mut scope = Scope::new();
// Create the `CheckCodePrefix` definition.
let mut gen = scope
.new_enum("CheckCodePrefix")
.vis("pub")
.derive("EnumString")
.derive("Debug")
.derive("PartialEq")
.derive("Eq")
.derive("Clone")
.derive("Serialize")
.derive("Deserialize");
for (prefix, _) in &prefix_to_codes {
gen = gen.push_variant(Variant::new(prefix.to_string()));
}
// Create the `PrefixSpecificity` definition.
scope
.new_enum("PrefixSpecificity")
.vis("pub")
.derive("PartialEq")
.derive("Eq")
.derive("PartialOrd")
.derive("Ord")
.push_variant(Variant::new("Category"))
.push_variant(Variant::new("Hundreds"))
.push_variant(Variant::new("Tens"))
.push_variant(Variant::new("Explicit"));
// Create the `match` statement, to map from definition to relevant codes.
let mut gen = scope
.new_impl("CheckCodePrefix")
.new_fn("codes")
.arg_ref_self()
.ret(Type::new("Vec<CheckCode>"))
.vis("pub")
.line("match self {");
for (prefix, codes) in &prefix_to_codes {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => vec![{}],",
codes
.iter()
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
}
gen.line("}");
// Create the `match` statement, to map from definition to specificity.
let mut gen = scope
.new_impl("CheckCodePrefix")
.new_fn("specificity")
.arg_ref_self()
.ret(Type::new("PrefixSpecificity"))
.vis("pub")
.line("match self {");
for (prefix, _) in &prefix_to_codes {
let specificity = match prefix.len() {
4 => "Explicit",
3 => "Tens",
2 => "Hundreds",
1 => "Category",
_ => panic!("Invalid prefix: {}", prefix),
};
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => PrefixSpecificity::{},",
specificity
));
}
gen.line("}");
println!("//! File automatically generated by examples/generate_check_code_prefix.rs.");
println!();
println!("use serde::{{Deserialize, Serialize}};");
println!("use strum_macros::EnumString;");
println!();
println!("use crate::checks::CheckCode;");
println!();
println!("{}", scope.to_string());
}

View File

@@ -19,7 +19,7 @@ fn main() {
"| {} | {} | {} | {} |",
check_kind.code().as_ref(),
check_kind.as_ref(),
check_kind.body().replace("|", r"\|"),
check_kind.summary().replace("|", r"\|"),
fix_token
);
}

20
resources/test/fixtures/B002.py vendored Normal file
View File

@@ -0,0 +1,20 @@
"""
Should emit:
B002 - on lines 15 and 20
"""
def this_is_all_fine(n):
x = n + 1
y = 1 + n
z = +x + y
return +z
def this_is_buggy(n):
x = ++n
return x
def this_is_buggy_too(n):
return ++n

187
resources/test/fixtures/B006_B008.py vendored Normal file
View File

@@ -0,0 +1,187 @@
import collections
import datetime as dt
import logging
import operator
import random
import re
import time
import types
from operator import attrgetter, itemgetter, methodcaller
from types import MappingProxyType
# B006
# Allow immutable literals/calls/comprehensions
def this_is_okay(value=(1, 2, 3)):
...
async def and_this_also(value=tuple()):
pass
def frozenset_also_okay(value=frozenset()):
pass
def mappingproxytype_okay(
value=MappingProxyType({}), value2=types.MappingProxyType({})
):
pass
def re_compile_ok(value=re.compile("foo")):
pass
def operators_ok(
v=operator.attrgetter("foo"),
v2=operator.itemgetter("foo"),
v3=operator.methodcaller("foo"),
):
pass
def operators_ok_unqualified(
v=attrgetter("foo"),
v2=itemgetter("foo"),
v3=methodcaller("foo"),
):
pass
def kwonlyargs_immutable(*, value=()):
...
# Flag mutable literals/comprehensions
def this_is_wrong(value=[1, 2, 3]):
...
def this_is_also_wrong(value={}):
...
def and_this(value=set()):
...
def this_too(value=collections.OrderedDict()):
...
async def async_this_too(value=collections.defaultdict()):
...
def dont_forget_me(value=collections.deque()):
...
# N.B. we're also flagging the function call in the comprehension
def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
pass
def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
pass
def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
pass
def kwonlyargs_mutable(*, value=[]):
...
# Recommended approach for mutable defaults
def do_this_instead(value=None):
if value is None:
value = set()
# B008
# Flag function calls as default args (including if they are part of a sub-expression)
def in_fact_all_calls_are_wrong(value=time.time()):
...
def f(when=dt.datetime.now() + dt.timedelta(days=7)):
pass
def can_even_catch_lambdas(a=(lambda x: x)()):
...
# Recommended approach for function calls as default args
LOGGER = logging.getLogger(__name__)
def do_this_instead_of_calls_in_defaults(logger=LOGGER):
# That makes it more obvious that this one value is reused.
...
# Handle inf/infinity/nan special case
def float_inf_okay(value=float("inf")):
pass
def float_infinity_okay(value=float("infinity")):
pass
def float_plus_infinity_okay(value=float("+infinity")):
pass
def float_minus_inf_okay(value=float("-inf")):
pass
def float_nan_okay(value=float("nan")):
pass
def float_minus_NaN_okay(value=float("-NaN")):
pass
def float_infinity_literal(value=float("1e999")):
pass
# But don't allow standard floats
def float_int_is_wrong(value=float(3)):
pass
def float_str_not_inf_or_nan_is_wrong(value=float("3.14")):
pass
# B006 and B008
# We should handle arbitrary nesting of these B008.
def nested_combo(a=[float(3), dt.datetime.now()]):
pass
# Don't flag nested B006 since we can't guarantee that
# it isn't made mutable by the outer operation.
def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
pass
# B008-ception.
def nested_b008(a=random.randint(0, dt.datetime.now().year)):
pass
# Ignore lambda contents since they are evaluated at call time.
def foo(f=lambda x: print(x)):
f(1)

31
resources/test/fixtures/B007.py vendored Normal file
View File

@@ -0,0 +1,31 @@
for i in range(10):
print(i)
print(i) # name no longer defined on Python 3; no warning yet
for i in range(10): # name not used within the loop; B007
print(10)
print(i) # name no longer defined on Python 3; no warning yet
for _ in range(10): # _ is okay for a throw-away variable
print(10)
for i in range(10):
for j in range(10):
for k in range(10): # k not used, i and j used transitively
print(i + j)
def strange_generator():
for i in range(10):
for j in range(10):
for k in range(10):
for l in range(10):
yield i, (j, (k, l))
for i, (j, (k, l)) in strange_generator(): # i, k not used
print(j, l)

8
resources/test/fixtures/B013.py vendored Normal file
View File

@@ -0,0 +1,8 @@
try:
pass
except (ValueError,):
pass
except AttributeError:
pass
except (ImportError, TypeError):
pass

36
resources/test/fixtures/B017.py vendored Normal file
View File

@@ -0,0 +1,36 @@
"""
Should emit:
B017 - on lines 20
"""
import asyncio
import unittest
CONSTANT = True
def something_else() -> None:
for i in (1, 2, 3):
print(i)
class Foo:
pass
class Foobar(unittest.TestCase):
def evil_raises(self) -> None:
with self.assertRaises(Exception):
raise Exception("Evil I say!")
def context_manager_raises(self) -> None:
with self.assertRaises(Exception) as ex:
raise Exception("Context manager is good")
self.assertEqual("Context manager is good", str(ex.exception))
def regex_raises(self) -> None:
with self.assertRaisesRegex(Exception, "Regex is good"):
raise Exception("Regex is good")
def raises_with_absolute_reference(self):
with self.assertRaises(asyncio.CancelledError):
Foo()

View File

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

View File

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

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

@@ -0,0 +1,5 @@
"""Test: removal of multi-segment and aliases imports."""
from a.b import c
from d.e import f as g
import h.i
import j.k as l

View File

@@ -1,3 +1,6 @@
import unittest
def Bad():
pass
@@ -24,3 +27,15 @@ def _good():
def good_func():
pass
def tearDownModule():
pass
class Test(unittest.TestCase):
def tearDown(self):
return super().tearDown()
def testTest(self):
assert True

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

@@ -0,0 +1,4 @@
def f():
lower = 0
Camel = 0
CONSTANT = 0

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

@@ -0,0 +1,15 @@
def __bad__():
pass
def __good():
pass
def good__():
pass
class Class:
def __good__(self):
pass

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

@@ -0,0 +1,3 @@
import mod.CONST as const
from mod import CONSTANT as constant
from mod import ANOTHER_CONSTANT as another_constant

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

@@ -0,0 +1,3 @@
import modl.lowercase as Lower
from mod import lowercase as Lowercase
from mod import another_lowercase as AnotherLowercase

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

@@ -0,0 +1,3 @@
import mod.Camel as camel
from mod import CamelCase as camelcase
from mod import AnotherCamelCase as another_camelcase

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

@@ -0,0 +1,3 @@
import mod.Camel as CAMEL
from mod import CamelCase as CAMELCASE
from mod import AnotherCamelCase as ANOTHER_CAMELCASE

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

@@ -0,0 +1,6 @@
class C:
lower = 0
CONSTANT = 0
mixedCase = 0
_mixedCase = 0
mixed_Case = 0

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

@@ -0,0 +1,5 @@
lower = 0
CONSTANT = 0
mixedCase = 0
_mixedCase = 0
mixed_Case = 0

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

@@ -0,0 +1,2 @@
import mod.CaMel as CM
from mod import CamelCase as CC

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

@@ -0,0 +1,10 @@
class Error(Exception):
pass
class AnotherError(Exception):
pass
class C(Exception):
pass

35
resources/test/fixtures/W605.py vendored Normal file
View File

@@ -0,0 +1,35 @@
#: W605:1:10
regex = '\.png$'
#: W605:2:1
regex = '''
\.png$
'''
#: W605:2:6
f(
'\_'
)
#: W605:4:6
"""
multi-line
literal
with \_ somewhere
in the middle
"""
#: Okay
regex = r'\.png$'
regex = '\\.png$'
regex = r'''
\.png$
'''
regex = r'''
\\.png$
'''
s = '\\'
regex = '\w' # noqa
regex = '''
\w
''' # noqa

View File

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

View File

@@ -0,0 +1,38 @@
"""
Double quotes multiline module docstring
"""
"""
this is not a docstring
"""
l = []
class Cls:
"""
Double quotes multiline class docstring
"""
"""
this is not a docstring
"""
# The colon in the list indexing below is an edge case for the docstring scanner
def f(self, bar="""
definitely not a docstring""",
val=l[Cls():3]):
"""
Double quotes multiline function docstring
"""
some_expression = 'hello world'
"""
this is not a docstring
"""
if l:
"""
Looks like a docstring, but in reality it isn't - only modules, classes and functions
"""
pass

View File

@@ -0,0 +1,9 @@
class SingleLineDocstrings():
""" Double quotes single line class docstring """
""" Not a docstring """
def foo(self, bar="""not a docstring"""):
""" Double quotes single line method docstring"""
pass
class Nested(foo()[:]): """ inline docstring """; pass

View File

@@ -0,0 +1,22 @@
def foo():
"""function without params, single line docstring"""
""" not a docstring"""
return
def foo2():
"""
function without params, multiline docstring
"""
""" not a docstring"""
return
def fun_with_params_no_docstring(a, b="""
not a
""" """docstring"""):
pass
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
""" not a docstring """):
pass

View File

@@ -0,0 +1,11 @@
"""
Double quotes multiline module docstring
"""
"""
this is not a docstring
"""
def foo():
pass
"""
this is not a docstring
"""

View File

@@ -0,0 +1,6 @@
""" Double quotes singleline module docstring """
""" this is not a docstring """
def foo():
pass
""" this is not a docstring """

View File

@@ -0,0 +1,40 @@
'''
Single quotes multiline module docstring
'''
'''
this is not a docstring
'''
l = []
class Cls(MakeKlass('''
class params \t not a docstring
''')):
'''
Single quotes multiline class docstring
'''
'''
this is not a docstring
'''
# The colon in the list indexing below is an edge case for the docstring scanner
def f(self, bar='''
definitely not a docstring''',
val=l[Cls():3]):
'''
Single quotes multiline function docstring
'''
some_expression = 'hello world'
'''
this is not a docstring
'''
if l:
'''
Looks like a docstring, but in reality it isn't - only modules, classes and functions
'''
pass

View File

@@ -0,0 +1,9 @@
class SingleLineDocstrings():
''' Double quotes single line class docstring '''
''' Not a docstring '''
def foo(self, bar='''not a docstring'''):
''' Double quotes single line method docstring'''
pass
class Nested(foo()[:]): ''' inline docstring '''; pass

View File

@@ -0,0 +1,23 @@
def foo():
'''function without params, single line docstring'''
''' not a docstring'''
return
def foo2():
'''
function without params, multiline docstring
'''
''' not a docstring'''
return
def fun_with_params_no_docstring(a, b='''
not a
''' '''docstring'''):
pass
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
''' not a docstring '''):
pass

View File

@@ -0,0 +1,11 @@
'''
Double quotes multiline module docstring
'''
'''
this is not a docstring
'''
def foo():
pass
'''
this is not a docstring
'''

View File

@@ -0,0 +1,6 @@
''' Double quotes singleline module docstring '''
''' this is not a docstring '''
def foo():
pass
''' this is not a docstring '''

View File

@@ -0,0 +1,2 @@
this_should_be_linted = "double quote string"
this_should_be_linted = u"double quote string"

View File

@@ -0,0 +1,5 @@
this_should_raise_Q003 = 'This is a \'string\''
this_is_fine = '"This" is a \'string\''
this_is_fine = "This is a 'string'"
this_is_fine = "\"This\" is a 'string'"
this_is_fine = r'This is a \'string\''

View File

@@ -0,0 +1,9 @@
s = """ This "should"
be
"linted" """
s = ''' This "should"
"not" be
"linted" '''
s = """'This should not be linted due to having would-be quadruple end quote'"""

View File

@@ -0,0 +1 @@
this_should_not_be_linted = "double quote string" # noqa

View File

@@ -0,0 +1,2 @@
s = 'double "quotes" wrapped in singles are ignored'
s = "single 'quotes' wrapped in doubles are ignored"

View File

@@ -0,0 +1,2 @@
this_should_be_linted = 'single quote string'
this_should_be_linted = u'double quote string'

View File

@@ -0,0 +1,5 @@
this_should_raise_Q003 = "This is a \"string\""
this_is_fine = "'This' is a \"string\""
this_is_fine = 'This is a "string"'
this_is_fine = '\'This\' is a "string"'
this_is_fine = r"This is a \"string\""

View File

@@ -0,0 +1,9 @@
s = ''' This 'should'
be
'linted' '''
s = """ This 'should'
'not' be
'linted' """
s = '''"This should not be linted due to having would-be quadruple end quote"'''

View File

@@ -0,0 +1 @@
this_should_not_be_linted = 'single quote string' # noqa

View File

@@ -0,0 +1,2 @@
s = "single 'quotes' wrapped in doubles are ignored"
s = 'double "quotes" wrapped in singles are ignored'

View File

@@ -5,3 +5,34 @@ extend-exclude = [
"migrations",
"directory/also_excluded.py",
]
per-file-ignores = [
"__init__.py:F401",
]
[tool.ruff.flake8-quotes]
inline-quotes = "single"
multiline-quotes = "double"
docstring-quotes = "double"
avoid-escape = true
[tool.ruff.pep8-naming]
ignore-names = [
"setUp",
"tearDown",
"setUpClass",
"tearDownClass",
"setUpModule",
"tearDownModule",
"asyncSetUp",
"asyncTearDown",
"setUpTestData",
"failureException",
"longMessage",
"maxDiff",
]
classmethod-decorators = [
"classmethod",
]
staticmethod-decorators = [
"staticmethod",
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, StmtKind};
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, StmtKind};
use crate::python::typing;
@@ -131,3 +131,15 @@ pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
false
}
}
/// Convert a location within a file (relative to `base`) to an absolute position.
pub fn to_absolute(relative: &Location, base: &Location) -> Location {
if relative.row() == 1 {
Location::new(
relative.row() + base.row() - 1,
relative.column() + base.column(),
)
} else {
Location::new(relative.row() + base.row() - 1, relative.column())
}
}

View File

@@ -1,4 +1,3 @@
pub mod checkers;
pub mod helpers;
pub mod operations;
pub mod relocate;

View File

@@ -1,6 +1,6 @@
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::{BindingKind, Range, Scope};
use crate::ast::types::{BindingKind, Scope};
/// Extract the names bound to a given __all__ assignment.
pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
@@ -117,69 +117,3 @@ pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
}
false
}
/// Struct used to efficiently slice source code at (row, column) Locations.
pub struct SourceCodeLocator<'a> {
content: &'a str,
offsets: Vec<Vec<usize>>,
initialized: bool,
}
impl<'a> SourceCodeLocator<'a> {
pub fn new(content: &'a str) -> Self {
SourceCodeLocator {
content,
offsets: vec![],
initialized: false,
}
}
fn init(&mut self) {
if !self.initialized {
let mut offset = 0;
for line in self.content.lines() {
let mut newline = 0;
let mut line_offsets: Vec<usize> = vec![];
for (i, _char) in line.char_indices() {
line_offsets.push(offset + i);
newline = i + 1;
}
line_offsets.push(offset + newline);
self.offsets.push(line_offsets);
offset += newline + 1;
}
self.offsets.push(vec![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 {
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

@@ -4,14 +4,14 @@ use crate::ast::types::Range;
fn relocate_keyword(keyword: &mut Keyword, location: Range) {
keyword.location = location.location;
keyword.end_location = location.end_location;
keyword.end_location = Some(location.end_location);
relocate_expr(&mut keyword.node.value, location);
}
/// Change an expression's location (recursively) to match a desired, fixed location.
pub fn relocate_expr(expr: &mut Expr, location: Range) {
expr.location = location.location;
expr.end_location = location.end_location;
expr.end_location = Some(location.end_location);
match &mut expr.node {
ExprKind::BoolOp { values, .. } => {
for expr in values {

View File

@@ -18,7 +18,9 @@ impl Range {
pub fn from_located<T>(located: &Located<T>) -> Self {
Range {
location: located.location,
end_location: located.end_location,
end_location: located
.end_location
.expect("AST nodes should have end_location."),
}
}
}

View File

@@ -1,2 +0,0 @@
pub mod fixer;
pub mod fixes;

View File

@@ -1,7 +1,11 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustpython_parser::ast::Location;
use crate::checks::{Check, Fix};
use crate::autofix::Fix;
use crate::autofix::Patch;
use crate::checks::Check;
#[derive(Hash)]
pub enum Mode {
@@ -10,6 +14,17 @@ pub enum Mode {
None,
}
impl Mode {
/// Return `true` if a patch should be generated under the given `Mode`.
pub fn patch(&self) -> bool {
match &self {
Mode::Generate => true,
Mode::Apply => true,
Mode::None => false,
}
}
}
impl From<bool> for Mode {
fn from(value: bool) -> Self {
match value {
@@ -35,34 +50,43 @@ pub fn fix_file(checks: &mut [Check], contents: &str) -> Option<String> {
fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) -> String {
let lines: Vec<&str> = contents.lines().collect();
let mut output = "".to_string();
let mut last_pos: Location = Location::new(0, 0);
let mut output: String = Default::default();
let mut last_pos: Location = Default::default();
let mut applied: BTreeSet<&Patch> = Default::default();
for fix in fixes.sorted_by_key(|fix| fix.location) {
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
if last_pos > fix.location {
for fix in fixes.sorted_by_key(|fix| fix.patch.location) {
// If we already applied an identical fix as part of another correction, skip any
// re-application.
if applied.contains(&fix.patch) {
fix.applied = true;
continue;
}
if fix.location.row() > last_pos.row() {
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
if last_pos > fix.patch.location {
continue;
}
if fix.patch.location.row() > last_pos.row() {
if last_pos.row() > 0 || last_pos.column() > 0 {
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
output.push_str(&lines[last_pos.row() - 1][last_pos.column()..]);
output.push('\n');
}
for line in &lines[last_pos.row()..fix.location.row() - 1] {
for line in &lines[last_pos.row()..fix.patch.location.row() - 1] {
output.push_str(line);
output.push('\n');
}
output.push_str(&lines[fix.location.row() - 1][..fix.location.column() - 1]);
output.push_str(&fix.content);
output.push_str(&lines[fix.patch.location.row() - 1][..fix.patch.location.column()]);
output.push_str(&fix.patch.content);
} else {
output.push_str(
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.location.column() - 1],
&lines[last_pos.row() - 1][last_pos.column()..fix.patch.location.column()],
);
output.push_str(&fix.content);
output.push_str(&fix.patch.content);
}
last_pos = fix.patch.end_location;
last_pos = fix.end_location;
applied.insert(&fix.patch);
fix.applied = true;
}
@@ -70,7 +94,7 @@ fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) ->
&& (last_pos.row() - 1) < lines.len()
&& (last_pos.row() > 0 || last_pos.column() > 0)
{
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
output.push_str(&lines[last_pos.row() - 1][last_pos.column()..]);
output.push('\n');
}
if last_pos.row() < lines.len() {
@@ -89,7 +113,8 @@ mod tests {
use rustpython_parser::ast::Location;
use crate::autofix::fixer::apply_fixes;
use crate::checks::Fix;
use crate::autofix::Fix;
use crate::autofix::Patch;
#[test]
fn empty_file() -> Result<()> {
@@ -105,9 +130,11 @@ mod tests {
#[test]
fn apply_single_replacement() -> Result<()> {
let mut fixes = vec![Fix {
content: "Bar".to_string(),
location: Location::new(1, 9),
end_location: Location::new(1, 15),
patch: Patch {
content: "Bar".to_string(),
location: Location::new(1, 8),
end_location: Location::new(1, 14),
},
applied: false,
}];
let actual = apply_fixes(
@@ -129,9 +156,11 @@ mod tests {
#[test]
fn apply_single_removal() -> Result<()> {
let mut fixes = vec![Fix {
content: "".to_string(),
location: Location::new(1, 8),
end_location: Location::new(1, 16),
patch: Patch {
content: "".to_string(),
location: Location::new(1, 7),
end_location: Location::new(1, 15),
},
applied: false,
}];
let actual = apply_fixes(
@@ -154,15 +183,19 @@ mod tests {
fn apply_double_removal() -> Result<()> {
let mut fixes = vec![
Fix {
content: "".to_string(),
location: Location::new(1, 8),
end_location: Location::new(1, 17),
patch: Patch {
content: "".to_string(),
location: Location::new(1, 7),
end_location: Location::new(1, 16),
},
applied: false,
},
Fix {
content: "".to_string(),
location: Location::new(1, 17),
end_location: Location::new(1, 24),
patch: Patch {
content: "".to_string(),
location: Location::new(1, 16),
end_location: Location::new(1, 23),
},
applied: false,
},
];
@@ -186,15 +219,19 @@ mod tests {
fn ignore_overlapping_fixes() -> Result<()> {
let mut fixes = vec![
Fix {
content: "".to_string(),
location: Location::new(1, 8),
end_location: Location::new(1, 16),
patch: Patch {
content: "".to_string(),
location: Location::new(1, 7),
end_location: Location::new(1, 15),
},
applied: false,
},
Fix {
content: "ignored".to_string(),
location: Location::new(1, 10),
end_location: Location::new(1, 12),
patch: Patch {
content: "ignored".to_string(),
location: Location::new(1, 9),
end_location: Location::new(1, 11),
},
applied: false,
},
];

View File

@@ -1,392 +0,0 @@
use anyhow::Result;
use itertools::Itertools;
use libcst_native::ImportNames::Aliases;
use libcst_native::NameOrAttribute::N;
use libcst_native::{Codegen, Expression, SmallStatement, Statement};
use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, StmtKind};
use rustpython_parser::lexer;
use rustpython_parser::token::Tok;
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::Range;
use crate::checks::Fix;
/// Convert a location within a file (relative to `base`) to an absolute position.
fn to_absolute(relative: &Location, base: &Location) -> Location {
if relative.row() == 1 {
Location::new(
relative.row() + base.row() - 1,
relative.column() + base.column() - 1,
)
} else {
Location::new(relative.row() + base.row() - 1, relative.column())
}
}
/// Generate a fix to remove a base from a ClassDef statement.
pub fn remove_class_def_base(
locator: &mut SourceCodeLocator,
stmt_at: &Location,
expr_at: Location,
bases: &[Expr],
keywords: &[Keyword],
) -> Option<Fix> {
let content = locator.slice_source_code_at(stmt_at);
// Case 1: `object` is the only base.
if bases.len() == 1 && keywords.is_empty() {
let mut fix_start = None;
let mut fix_end = None;
let mut count: usize = 0;
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
if matches!(tok, Tok::Lpar) {
if count == 0 {
fix_start = Some(to_absolute(&start, stmt_at));
}
count += 1;
}
if matches!(tok, Tok::Rpar) {
count -= 1;
if count == 0 {
fix_end = Some(to_absolute(&end, stmt_at));
break;
}
}
}
return match (fix_start, fix_end) {
(Some(start), Some(end)) => Some(Fix {
content: "".to_string(),
location: start,
end_location: end,
applied: false,
}),
_ => None,
};
}
if bases
.iter()
.map(|node| node.location)
.chain(keywords.iter().map(|node| node.location))
.any(|location| location > expr_at)
{
// Case 2: `object` is _not_ the last node.
let mut fix_start: Option<Location> = None;
let mut fix_end: Option<Location> = None;
let mut seen_comma = false;
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
let start = to_absolute(&start, stmt_at);
if seen_comma {
if matches!(tok, Tok::Newline) {
fix_end = Some(end);
} else {
fix_end = Some(start);
}
break;
}
if start == expr_at {
fix_start = Some(start);
}
if fix_start.is_some() && matches!(tok, Tok::Comma) {
seen_comma = true;
}
}
match (fix_start, fix_end) {
(Some(start), Some(end)) => Some(Fix {
content: "".to_string(),
location: start,
end_location: end,
applied: false,
}),
_ => None,
}
} else {
// Case 3: `object` is the last node, so we have to find the last token that isn't a comma.
let mut fix_start: Option<Location> = None;
let mut fix_end: Option<Location> = None;
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
let start = to_absolute(&start, stmt_at);
let end = to_absolute(&end, stmt_at);
if start == expr_at {
fix_end = Some(end);
break;
}
if matches!(tok, Tok::Comma) {
fix_start = Some(start);
}
}
match (fix_start, fix_end) {
(Some(start), Some(end)) => Some(Fix {
content: "".to_string(),
location: start,
end_location: end,
applied: false,
}),
_ => None,
}
}
}
pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> Option<Fix> {
let range = Range::from_located(expr);
let contents = locator.slice_source_code_range(&range);
let mut tree = match libcst_native::parse_module(contents, None) {
Ok(m) => m,
Err(_) => return None,
};
if let Some(Statement::Simple(body)) = tree.body.first_mut() {
if let Some(SmallStatement::Expr(body)) = body.body.first_mut() {
if let Expression::Call(body) = &mut body.value {
body.args = vec![];
body.whitespace_before_args = Default::default();
body.whitespace_after_func = Default::default();
let mut state = Default::default();
tree.codegen(&mut state);
return Some(Fix {
content: state.to_string(),
location: range.location,
end_location: range.end_location,
applied: false,
});
}
}
}
None
}
/// Determine if a body contains only a single statement, taking into account deleted.
fn has_single_child(body: &[Stmt], deleted: &[&Stmt]) -> bool {
body.iter().filter(|child| !deleted.contains(child)).count() == 1
}
/// Determine if a child is the only statement in its body.
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
match &parent.node {
StmtKind::FunctionDef { body, .. }
| StmtKind::AsyncFunctionDef { body, .. }
| StmtKind::ClassDef { body, .. }
| StmtKind::With { body, .. }
| StmtKind::AsyncWith { body, .. } => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else {
Err(anyhow::anyhow!("Unable to find child in parent body."))
}
}
StmtKind::For { body, orelse, .. }
| StmtKind::AsyncFor { body, orelse, .. }
| StmtKind::While { body, orelse, .. }
| StmtKind::If { body, orelse, .. } => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else if orelse.iter().contains(child) {
Ok(has_single_child(orelse, deleted))
} else {
Err(anyhow::anyhow!("Unable to find child in parent body."))
}
}
StmtKind::Try {
body,
handlers,
orelse,
finalbody,
} => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else if orelse.iter().contains(child) {
Ok(has_single_child(orelse, deleted))
} else if finalbody.iter().contains(child) {
Ok(has_single_child(finalbody, deleted))
} else if let Some(body) = handlers.iter().find_map(|handler| match &handler.node {
ExcepthandlerKind::ExceptHandler { body, .. } => {
if body.iter().contains(child) {
Some(body)
} else {
None
}
}
}) {
Ok(has_single_child(body, deleted))
} else {
Err(anyhow::anyhow!("Unable to find child in parent body."))
}
}
_ => Err(anyhow::anyhow!("Unable to find child in parent body.")),
}
}
pub fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
if parent
.map(|parent| is_lone_child(stmt, parent, deleted))
.map_or(Ok(None), |v| v.map(Some))?
.unwrap_or_default()
{
// If removing this node would lead to an invalid syntax tree, replace
// it with a `pass`.
Ok(Fix {
location: stmt.location,
end_location: stmt.end_location,
content: "pass".to_string(),
applied: false,
})
} else {
// Otherwise, nuke the entire line.
// TODO(charlie): This logic assumes that there are no multi-statement physical lines.
Ok(Fix {
location: Location::new(stmt.location.row(), 1),
end_location: Location::new(stmt.end_location.row() + 1, 1),
content: "".to_string(),
applied: false,
})
}
}
/// Generate a Fix to remove any unused imports from an `import` statement.
pub fn remove_unused_imports(
locator: &mut SourceCodeLocator,
full_names: &[&str],
stmt: &Stmt,
parent: Option<&Stmt>,
deleted: &[&Stmt],
) -> Result<Fix> {
let mut tree = match libcst_native::parse_module(
locator.slice_source_code_range(&Range::from_located(stmt)),
None,
) {
Ok(m) => m,
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
};
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
body
} else {
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
};
let body = if let Some(SmallStatement::Import(body)) = body.body.first_mut() {
body
} else {
return Err(anyhow::anyhow!(
"Expected node to be: SmallStatement::ImportFrom."
));
};
let aliases = &mut body.names;
// Preserve the trailing comma (or not) from the last entry.
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
// Identify unused imports from within the `import from`.
let mut removable = vec![];
for (index, alias) in aliases.iter().enumerate() {
if let N(import_name) = &alias.name {
if full_names.contains(&import_name.value) {
removable.push(index);
}
}
}
// TODO(charlie): This is quadratic.
for index in removable.iter().rev() {
aliases.remove(*index);
}
if let Some(alias) = aliases.last_mut() {
alias.comma = trailing_comma;
}
if aliases.is_empty() {
remove_stmt(stmt, parent, deleted)
} else {
let mut state = Default::default();
tree.codegen(&mut state);
Ok(Fix {
content: state.to_string(),
location: stmt.location,
end_location: stmt.end_location,
applied: false,
})
}
}
/// Generate a Fix to remove any unused imports from an `import from` statement.
pub fn remove_unused_import_froms(
locator: &mut SourceCodeLocator,
full_names: &[&str],
stmt: &Stmt,
parent: Option<&Stmt>,
deleted: &[&Stmt],
) -> Result<Fix> {
let mut tree = match libcst_native::parse_module(
locator.slice_source_code_range(&Range::from_located(stmt)),
None,
) {
Ok(m) => m,
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
};
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
body
} else {
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
};
let body = if let Some(SmallStatement::ImportFrom(body)) = body.body.first_mut() {
body
} else {
return Err(anyhow::anyhow!(
"Expected node to be: SmallStatement::ImportFrom."
));
};
let aliases = if let Aliases(aliases) = &mut body.names {
aliases
} else {
return Err(anyhow::anyhow!("Expected node to be: Aliases."));
};
// Preserve the trailing comma (or not) from the last entry.
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
// Identify unused imports from within the `import from`.
let mut removable = vec![];
for (index, alias) in aliases.iter().enumerate() {
if let N(name) = &alias.name {
let import_name = if let Some(N(module_name)) = &body.module {
format!("{}.{}", module_name.value, name.value)
} else {
name.value.to_string()
};
if full_names.contains(&import_name.as_str()) {
removable.push(index);
}
}
}
// TODO(charlie): This is quadratic.
for index in removable.iter().rev() {
aliases.remove(*index);
}
if let Some(alias) = aliases.last_mut() {
alias.comma = trailing_comma;
}
if aliases.is_empty() {
remove_stmt(stmt, parent, deleted)
} else {
let mut state = Default::default();
tree.codegen(&mut state);
Ok(Fix {
content: state.to_string(),
location: stmt.location,
end_location: stmt.end_location,
applied: false,
})
}
}

89
src/autofix/helpers.rs Normal file
View File

@@ -0,0 +1,89 @@
use anyhow::Result;
use itertools::Itertools;
use rustpython_parser::ast::{ExcepthandlerKind, Location, Stmt, StmtKind};
use crate::autofix::Fix;
/// Determine if a body contains only a single statement, taking into account deleted.
fn has_single_child(body: &[Stmt], deleted: &[&Stmt]) -> bool {
body.iter().filter(|child| !deleted.contains(child)).count() == 1
}
/// Determine if a child is the only statement in its body.
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
match &parent.node {
StmtKind::FunctionDef { body, .. }
| StmtKind::AsyncFunctionDef { body, .. }
| StmtKind::ClassDef { body, .. }
| StmtKind::With { body, .. }
| StmtKind::AsyncWith { body, .. } => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else {
Err(anyhow::anyhow!("Unable to find child in parent body."))
}
}
StmtKind::For { body, orelse, .. }
| StmtKind::AsyncFor { body, orelse, .. }
| StmtKind::While { body, orelse, .. }
| StmtKind::If { body, orelse, .. } => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else if orelse.iter().contains(child) {
Ok(has_single_child(orelse, deleted))
} else {
Err(anyhow::anyhow!("Unable to find child in parent body."))
}
}
StmtKind::Try {
body,
handlers,
orelse,
finalbody,
} => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else if orelse.iter().contains(child) {
Ok(has_single_child(orelse, deleted))
} else if finalbody.iter().contains(child) {
Ok(has_single_child(finalbody, deleted))
} else if let Some(body) = handlers.iter().find_map(|handler| match &handler.node {
ExcepthandlerKind::ExceptHandler { body, .. } => {
if body.iter().contains(child) {
Some(body)
} else {
None
}
}
}) {
Ok(has_single_child(body, deleted))
} else {
Err(anyhow::anyhow!("Unable to find child in parent body."))
}
}
_ => Err(anyhow::anyhow!("Unable to find child in parent body.")),
}
}
pub fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
if parent
.map(|parent| is_lone_child(stmt, parent, deleted))
.map_or(Ok(None), |v| v.map(Some))?
.unwrap_or_default()
{
// If removing this node would lead to an invalid syntax tree, replace
// it with a `pass`.
Ok(Fix::replacement(
"pass".to_string(),
stmt.location,
stmt.end_location.unwrap(),
))
} else {
// Otherwise, nuke the entire line.
// TODO(charlie): This logic assumes that there are no multi-statement physical lines.
Ok(Fix::deletion(
Location::new(stmt.location.row(), 0),
Location::new(stmt.end_location.unwrap().row() + 1, 0),
))
}
}

53
src/autofix/mod.rs Normal file
View File

@@ -0,0 +1,53 @@
use rustpython_ast::Location;
use serde::{Deserialize, Serialize};
pub mod fixer;
pub mod helpers;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Patch {
pub content: String,
pub location: Location,
pub end_location: Location,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fix {
pub patch: Patch,
pub applied: bool,
}
impl Fix {
pub fn deletion(start: Location, end: Location) -> Self {
Self {
patch: Patch {
content: "".to_string(),
location: start,
end_location: end,
},
applied: false,
}
}
pub fn replacement(content: String, start: Location, end: Location) -> Self {
Self {
patch: Patch {
content,
location: start,
end_location: end,
},
applied: false,
}
}
pub fn insertion(content: String, at: Location) -> Self {
Self {
patch: Patch {
content,
location: at,
end_location: at,
},
applied: false,
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,13 @@
//! Lint rules based on checking raw physical lines.
use std::collections::BTreeMap;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::autofix::Fix;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::noqa;
use crate::noqa::Directive;
use crate::settings::Settings;
@@ -91,8 +94,8 @@ pub fn check_lines(
let check = Check::new(
CheckKind::LineTooLong(line_length, settings.line_length),
Range {
location: Location::new(lineno + 1, 1),
end_location: Location::new(lineno + 1, line_length + 1),
location: Location::new(lineno + 1, 0),
end_location: Location::new(lineno + 1, line_length),
},
);
@@ -161,20 +164,15 @@ pub fn check_lines(
let mut check = Check::new(
CheckKind::UnusedNOQA(None),
Range {
location: Location::new(row + 1, start + 1),
end_location: Location::new(row + 1, end + 1),
location: Location::new(row + 1, start),
end_location: Location::new(row + 1, end),
},
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "".to_string(),
location: Location::new(row + 1, start + 1),
end_location: Location::new(
row + 1,
lines[row].chars().count() + 1,
),
applied: false,
});
if autofix.patch() {
check.amend(Fix::deletion(
Location::new(row + 1, start),
Location::new(row + 1, lines[row].chars().count()),
));
}
line_checks.push(check);
}
@@ -194,31 +192,22 @@ pub fn check_lines(
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(invalid_codes)),
Range {
location: Location::new(row + 1, start + 1),
end_location: Location::new(row + 1, end + 1),
location: Location::new(row + 1, start),
end_location: Location::new(row + 1, end),
},
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if autofix.patch() {
if valid_codes.is_empty() {
check.amend(Fix {
content: "".to_string(),
location: Location::new(row + 1, start + 1),
end_location: Location::new(
row + 1,
lines[row].chars().count() + 1,
),
applied: false,
});
check.amend(Fix::deletion(
Location::new(row + 1, start),
Location::new(row + 1, lines[row].chars().count()),
));
} else {
check.amend(Fix {
content: format!(" # noqa: {}", valid_codes.join(", ")),
location: Location::new(row + 1, start + 1),
end_location: Location::new(
row + 1,
lines[row].chars().count() + 1,
),
applied: false,
});
check.amend(Fix::replacement(
format!(" # noqa: {}", valid_codes.join(", ")),
Location::new(row + 1, start),
Location::new(row + 1, lines[row].chars().count()),
));
}
}
line_checks.push(check);
@@ -240,7 +229,7 @@ pub fn check_lines(
mod tests {
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::settings;
use crate::settings::Settings;
use super::check_lines;
@@ -254,9 +243,9 @@ mod tests {
&mut checks,
line,
&noqa_line_for,
&settings::Settings {
&Settings {
line_length,
..settings::Settings::for_rule(CheckCode::E501)
..Settings::for_rule(CheckCode::E501)
},
&fixer::Mode::Generate,
);

51
src/check_tokens.rs Normal file
View File

@@ -0,0 +1,51 @@
//! Lint rules based on token traversal.
use rustpython_parser::lexer::{LexResult, Tok};
use crate::checks::{Check, CheckCode};
use crate::flake8_quotes::docstring_detection::StateMachine;
use crate::source_code_locator::SourceCodeLocator;
use crate::{flake8_quotes, pycodestyle, Settings};
pub fn check_tokens(
checks: &mut Vec<Check>,
locator: &SourceCodeLocator,
tokens: &[LexResult],
settings: &Settings,
) {
let enforce_invalid_escape_sequence = settings.enabled.contains(&CheckCode::W605);
let enforce_quotes = settings.enabled.contains(&CheckCode::Q000)
| settings.enabled.contains(&CheckCode::Q001)
| settings.enabled.contains(&CheckCode::Q002)
| settings.enabled.contains(&CheckCode::Q003);
let mut state_machine = StateMachine::new();
for (start, tok, end) in tokens.iter().flatten() {
// W605
if enforce_invalid_escape_sequence {
if matches!(tok, Tok::String { .. }) {
checks.extend(pycodestyle::checks::invalid_escape_sequence(
locator, start, end,
));
}
}
// flake8-quotes
if enforce_quotes {
let is_docstring = state_machine.consume(tok);
if matches!(tok, Tok::String { .. }) {
if let Some(check) = flake8_quotes::checks::quotes(
locator,
start,
end,
is_docstring,
&settings.flake8_quotes,
) {
if settings.enabled.contains(check.kind.code()) {
checks.push(check);
}
}
}
}
}
}

View File

@@ -5,8 +5,10 @@ use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use strum_macros::{AsRefStr, EnumIter, EnumString};
use crate::ast::checkers::Primitive;
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::flake8_quotes::settings::Quote;
use crate::pyupgrade::types::Primitive;
#[derive(
AsRefStr,
@@ -40,6 +42,7 @@ pub enum CheckCode {
E999,
// pycodestyle warnings
W292,
W605,
// pyflakes
F401,
F402,
@@ -74,8 +77,13 @@ pub enum CheckCode {
A002,
A003,
// flake8-bugbear
B002,
B006,
B007,
B011,
B013,
B014,
B017,
B025,
// flake8-comprehensions
C400,
@@ -97,6 +105,11 @@ pub enum CheckCode {
// flake8-print
T201,
T203,
// flake8-quotes
Q000,
Q001,
Q002,
Q003,
// pyupgrade
U001,
U002,
@@ -157,6 +170,16 @@ pub enum CheckCode {
N803,
N804,
N805,
N806,
N807,
N811,
N812,
N813,
N814,
N815,
N816,
N817,
N818,
// Meta
M001,
}
@@ -164,7 +187,8 @@ pub enum CheckCode {
#[derive(EnumIter, Debug, PartialEq, Eq)]
pub enum CheckCategory {
Pyflakes,
Pycodestyle,
PycodestyleError,
PycodestyleWarning,
Pydocstyle,
Pyupgrade,
PEP8Naming,
@@ -172,18 +196,21 @@ pub enum CheckCategory {
Flake8Bugbear,
Flake8Builtins,
Flake8Print,
Flake8Quotes,
Meta,
}
impl CheckCategory {
pub fn title(&self) -> &'static str {
match self {
CheckCategory::Pycodestyle => "pycodestyle",
CheckCategory::PycodestyleError => "pycodestyle (error)",
CheckCategory::PycodestyleWarning => "pycodestyle (warning)",
CheckCategory::Pyflakes => "Pyflakes",
CheckCategory::Flake8Builtins => "flake8-builtins",
CheckCategory::Flake8Bugbear => "flake8-bugbear",
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
CheckCategory::Pyupgrade => "pyupgrade",
CheckCategory::Pydocstyle => "pydocstyle",
CheckCategory::PEP8Naming => "pep8-naming",
@@ -195,8 +222,9 @@ impl CheckCategory {
#[allow(clippy::upper_case_acronyms)]
pub enum LintSource {
AST,
Lines,
FileSystem,
Lines,
Tokens,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -211,18 +239,30 @@ pub enum CheckKind {
AmbiguousClassName(String),
AmbiguousFunctionName(String),
AmbiguousVariableName(String),
DoNotAssignLambda,
DoNotUseBareExcept,
IOError(String),
LineTooLong(usize, usize),
ModuleImportNotAtTopOfFile,
NoneComparison(RejectedCmpop),
NotInTest,
NotIsTest,
SyntaxError(String),
TrueFalseComparison(bool, RejectedCmpop),
TypeComparison,
// pycodestyle warnings
NoNewLineAtEndOfFile,
InvalidEscapeSequence(char),
// pyflakes
AssertTuple,
BreakOutsideLoop,
ContinueOutsideLoop,
DefaultExceptNotLast,
DoNotAssignLambda,
DoNotUseBareExcept,
DuplicateArgumentName,
ExpressionsInStarAssignment,
FStringMissingPlaceholders,
ForwardAnnotationSyntaxError(String),
FutureFeatureNotDefined(String),
IOError(String),
IfTuple,
ImportShadowedByLoopVar(String, usize),
ImportStarNotPermitted(String),
@@ -231,34 +271,29 @@ pub enum CheckKind {
InvalidPrintSyntax,
IsLiteral,
LateFutureImport,
LineTooLong(usize, usize),
ModuleImportNotAtTopOfFile,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable(String),
NoneComparison(RejectedCmpop),
NotInTest,
NotIsTest,
RaiseNotImplemented,
ReturnOutsideFunction,
SyntaxError(String),
TrueFalseComparison(bool, RejectedCmpop),
TwoStarredExpressions,
TypeComparison,
UndefinedExport(String),
UndefinedLocal(String),
UndefinedName(String),
UnusedImport(Vec<String>),
UnusedImport(Vec<String>, bool),
UnusedVariable(String),
YieldOutsideFunction,
// pycodestyle warnings
NoNewLineAtEndOfFile,
// flake8-builtin
// flake8-builtins
BuiltinVariableShadowing(String),
BuiltinArgumentShadowing(String),
BuiltinAttributeShadowing(String),
// flake8-bugbear
UnaryPrefixIncrement,
MutableArgumentDefault,
UnusedLoopControlVariable(String),
DoNotAssertFalse,
RedundantTupleInExceptionHandler(String),
DuplicateHandlerException(Vec<String>),
NoAssertRaisesException,
DuplicateTryBlockException(String),
// flake8-comprehensions
UnnecessaryGeneratorList,
@@ -280,6 +315,11 @@ pub enum CheckKind {
// flake8-print
PrintFound,
PPrintFound,
// flake8-quotes
BadQuotesInlineString(Quote),
BadQuotesMultilineString(Quote),
BadQuotesDocstring(Quote),
AvoidQuoteEscape,
// pyupgrade
TypeOfPrimitive(Primitive),
UnnecessaryAbspath,
@@ -292,6 +332,7 @@ pub enum CheckKind {
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
BlankLineAfterSummary,
BlankLineBeforeSection(String),
CapitalizeSectionName(String),
DashedUnderlineAfterSection(String),
@@ -307,7 +348,6 @@ pub enum CheckKind {
NewLineAfterLastParagraph,
NewLineAfterSectionName(String),
NoBlankLineAfterFunction(usize),
NoBlankLineAfterSummary,
NoBlankLineBeforeClass(usize),
NoBlankLineBeforeFunction(usize),
NoBlankLinesBetweenHeaderAndContent(String),
@@ -340,6 +380,16 @@ pub enum CheckKind {
InvalidArgumentName(String),
InvalidFirstArgumentNameForClassMethod,
InvalidFirstArgumentNameForMethod,
NonLowercaseVariableInFunction(String),
DunderFunctionName,
ConstantImportedAsNonConstant(String, String),
LowercaseImportedAsNonLowercase(String, String),
CamelcaseImportedAsLowercase(String, String),
CamelcaseImportedAsConstant(String, String),
MixedCaseVariableInClassScope(String),
MixedCaseVariableInGlobalScope(String),
CamelcaseImportedAsAcronym(String, String),
ErrorSuffixOnExceptionName(String),
// Meta
UnusedNOQA(Option<Vec<String>>),
}
@@ -349,6 +399,11 @@ impl CheckCode {
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E501 | CheckCode::W292 | CheckCode::M001 => &LintSource::Lines,
CheckCode::W605
| CheckCode::Q000
| CheckCode::Q001
| CheckCode::Q002
| CheckCode::Q003 => &LintSource::Tokens,
CheckCode::E902 => &LintSource::FileSystem,
_ => &LintSource::AST,
}
@@ -374,8 +429,9 @@ impl CheckCode {
CheckCode::E999 => CheckKind::SyntaxError("`...`".to_string()),
// pycodestyle warnings
CheckCode::W292 => CheckKind::NoNewLineAtEndOfFile,
CheckCode::W605 => CheckKind::InvalidEscapeSequence('c'),
// pyflakes
CheckCode::F401 => CheckKind::UnusedImport(vec!["...".to_string()]),
CheckCode::F401 => CheckKind::UnusedImport(vec!["...".to_string()], false),
CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1),
CheckCode::F403 => CheckKind::ImportStarUsed("...".to_string()),
CheckCode::F404 => CheckKind::LateFutureImport,
@@ -410,8 +466,15 @@ impl CheckCode {
CheckCode::A002 => CheckKind::BuiltinArgumentShadowing("...".to_string()),
CheckCode::A003 => CheckKind::BuiltinAttributeShadowing("...".to_string()),
// flake8-bugbear
CheckCode::B002 => CheckKind::UnaryPrefixIncrement,
CheckCode::B006 => CheckKind::MutableArgumentDefault,
CheckCode::B007 => CheckKind::UnusedLoopControlVariable("i".to_string()),
CheckCode::B011 => CheckKind::DoNotAssertFalse,
CheckCode::B013 => {
CheckKind::RedundantTupleInExceptionHandler("ValueError".to_string())
}
CheckCode::B014 => CheckKind::DuplicateHandlerException(vec!["ValueError".to_string()]),
CheckCode::B017 => CheckKind::NoAssertRaisesException,
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
// flake8-comprehensions
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
@@ -446,6 +509,11 @@ impl CheckCode {
// flake8-print
CheckCode::T201 => CheckKind::PrintFound,
CheckCode::T203 => CheckKind::PPrintFound,
// flake8-quotes
CheckCode::Q000 => CheckKind::BadQuotesInlineString(Quote::Double),
CheckCode::Q001 => CheckKind::BadQuotesMultilineString(Quote::Double),
CheckCode::Q002 => CheckKind::BadQuotesDocstring(Quote::Double),
CheckCode::Q003 => CheckKind::AvoidQuoteEscape,
// pyupgrade
CheckCode::U001 => CheckKind::UselessMetaclassType,
CheckCode::U002 => CheckKind::UnnecessaryAbspath,
@@ -472,7 +540,7 @@ impl CheckCode {
CheckCode::D202 => CheckKind::NoBlankLineAfterFunction(1),
CheckCode::D203 => CheckKind::OneBlankLineBeforeClass(0),
CheckCode::D204 => CheckKind::OneBlankLineAfterClass(0),
CheckCode::D205 => CheckKind::NoBlankLineAfterSummary,
CheckCode::D205 => CheckKind::BlankLineAfterSummary,
CheckCode::D206 => CheckKind::IndentWithSpaces,
CheckCode::D207 => CheckKind::NoUnderIndentation,
CheckCode::D208 => CheckKind::NoOverIndentation,
@@ -515,6 +583,26 @@ impl CheckCode {
CheckCode::N803 => CheckKind::InvalidArgumentName("...".to_string()),
CheckCode::N804 => CheckKind::InvalidFirstArgumentNameForClassMethod,
CheckCode::N805 => CheckKind::InvalidFirstArgumentNameForMethod,
CheckCode::N806 => CheckKind::NonLowercaseVariableInFunction("...".to_string()),
CheckCode::N807 => CheckKind::DunderFunctionName,
CheckCode::N811 => {
CheckKind::ConstantImportedAsNonConstant("...".to_string(), "...".to_string())
}
CheckCode::N812 => {
CheckKind::LowercaseImportedAsNonLowercase("...".to_string(), "...".to_string())
}
CheckCode::N813 => {
CheckKind::CamelcaseImportedAsLowercase("...".to_string(), "...".to_string())
}
CheckCode::N814 => {
CheckKind::CamelcaseImportedAsConstant("...".to_string(), "...".to_string())
}
CheckCode::N815 => CheckKind::MixedCaseVariableInClassScope("mixedCase".to_string()),
CheckCode::N816 => CheckKind::MixedCaseVariableInGlobalScope("mixedCase".to_string()),
CheckCode::N817 => {
CheckKind::CamelcaseImportedAsAcronym("...".to_string(), "...".to_string())
}
CheckCode::N818 => CheckKind::ErrorSuffixOnExceptionName("...".to_string()),
// Meta
CheckCode::M001 => CheckKind::UnusedNOQA(None),
}
@@ -522,21 +610,22 @@ impl CheckCode {
pub fn category(&self) -> CheckCategory {
match self {
CheckCode::E402 => CheckCategory::Pycodestyle,
CheckCode::E501 => CheckCategory::Pycodestyle,
CheckCode::E711 => CheckCategory::Pycodestyle,
CheckCode::E712 => CheckCategory::Pycodestyle,
CheckCode::E713 => CheckCategory::Pycodestyle,
CheckCode::E714 => CheckCategory::Pycodestyle,
CheckCode::E721 => CheckCategory::Pycodestyle,
CheckCode::E722 => CheckCategory::Pycodestyle,
CheckCode::E731 => CheckCategory::Pycodestyle,
CheckCode::E741 => CheckCategory::Pycodestyle,
CheckCode::E742 => CheckCategory::Pycodestyle,
CheckCode::E743 => CheckCategory::Pycodestyle,
CheckCode::E902 => CheckCategory::Pycodestyle,
CheckCode::E999 => CheckCategory::Pycodestyle,
CheckCode::W292 => CheckCategory::Pycodestyle,
CheckCode::E402 => CheckCategory::PycodestyleError,
CheckCode::E501 => CheckCategory::PycodestyleError,
CheckCode::E711 => CheckCategory::PycodestyleError,
CheckCode::E712 => CheckCategory::PycodestyleError,
CheckCode::E713 => CheckCategory::PycodestyleError,
CheckCode::E714 => CheckCategory::PycodestyleError,
CheckCode::E721 => CheckCategory::PycodestyleError,
CheckCode::E722 => CheckCategory::PycodestyleError,
CheckCode::E731 => CheckCategory::PycodestyleError,
CheckCode::E741 => CheckCategory::PycodestyleError,
CheckCode::E742 => CheckCategory::PycodestyleError,
CheckCode::E743 => CheckCategory::PycodestyleError,
CheckCode::E902 => CheckCategory::PycodestyleError,
CheckCode::E999 => CheckCategory::PycodestyleError,
CheckCode::W292 => CheckCategory::PycodestyleWarning,
CheckCode::W605 => CheckCategory::PycodestyleWarning,
CheckCode::F401 => CheckCategory::Pyflakes,
CheckCode::F402 => CheckCategory::Pyflakes,
CheckCode::F403 => CheckCategory::Pyflakes,
@@ -568,8 +657,13 @@ impl CheckCode {
CheckCode::A001 => CheckCategory::Flake8Builtins,
CheckCode::A002 => CheckCategory::Flake8Builtins,
CheckCode::A003 => CheckCategory::Flake8Builtins,
CheckCode::B002 => CheckCategory::Flake8Bugbear,
CheckCode::B006 => CheckCategory::Flake8Bugbear,
CheckCode::B007 => CheckCategory::Flake8Bugbear,
CheckCode::B011 => CheckCategory::Flake8Bugbear,
CheckCode::B013 => CheckCategory::Flake8Bugbear,
CheckCode::B014 => CheckCategory::Flake8Bugbear,
CheckCode::B017 => CheckCategory::Flake8Bugbear,
CheckCode::B025 => CheckCategory::Flake8Bugbear,
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
@@ -589,6 +683,10 @@ impl CheckCode {
CheckCode::C417 => CheckCategory::Flake8Comprehensions,
CheckCode::T201 => CheckCategory::Flake8Print,
CheckCode::T203 => CheckCategory::Flake8Print,
CheckCode::Q000 => CheckCategory::Flake8Quotes,
CheckCode::Q001 => CheckCategory::Flake8Quotes,
CheckCode::Q002 => CheckCategory::Flake8Quotes,
CheckCode::Q003 => CheckCategory::Flake8Quotes,
CheckCode::U001 => CheckCategory::Pyupgrade,
CheckCode::U002 => CheckCategory::Pyupgrade,
CheckCode::U003 => CheckCategory::Pyupgrade,
@@ -646,6 +744,16 @@ impl CheckCode {
CheckCode::N803 => CheckCategory::PEP8Naming,
CheckCode::N804 => CheckCategory::PEP8Naming,
CheckCode::N805 => CheckCategory::PEP8Naming,
CheckCode::N806 => CheckCategory::PEP8Naming,
CheckCode::N807 => CheckCategory::PEP8Naming,
CheckCode::N811 => CheckCategory::PEP8Naming,
CheckCode::N812 => CheckCategory::PEP8Naming,
CheckCode::N813 => CheckCategory::PEP8Naming,
CheckCode::N814 => CheckCategory::PEP8Naming,
CheckCode::N815 => CheckCategory::PEP8Naming,
CheckCode::N816 => CheckCategory::PEP8Naming,
CheckCode::N817 => CheckCategory::PEP8Naming,
CheckCode::N818 => CheckCategory::PEP8Naming,
CheckCode::M001 => CheckCategory::Meta,
}
}
@@ -695,18 +803,24 @@ impl CheckKind {
CheckKind::UndefinedExport(_) => &CheckCode::F822,
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
CheckKind::UndefinedName(_) => &CheckCode::F821,
CheckKind::UnusedImport(_) => &CheckCode::F401,
CheckKind::UnusedImport(_, _) => &CheckCode::F401,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
// pycodestyle warnings
CheckKind::NoNewLineAtEndOfFile => &CheckCode::W292,
CheckKind::InvalidEscapeSequence(_) => &CheckCode::W605,
// flake8-builtins
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
CheckKind::BuiltinAttributeShadowing(_) => &CheckCode::A003,
// flake8-bugbear
CheckKind::UnaryPrefixIncrement => &CheckCode::B002,
CheckKind::MutableArgumentDefault => &CheckCode::B006,
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
CheckKind::DoNotAssertFalse => &CheckCode::B011,
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
@@ -728,6 +842,11 @@ impl CheckKind {
// flake8-print
CheckKind::PrintFound => &CheckCode::T201,
CheckKind::PPrintFound => &CheckCode::T203,
// flake8-quotes
CheckKind::BadQuotesInlineString(_) => &CheckCode::Q000,
CheckKind::BadQuotesMultilineString(_) => &CheckCode::Q001,
CheckKind::BadQuotesDocstring(_) => &CheckCode::Q002,
CheckKind::AvoidQuoteEscape => &CheckCode::Q003,
// pyupgrade
CheckKind::TypeOfPrimitive(_) => &CheckCode::U003,
CheckKind::UnnecessaryAbspath => &CheckCode::U002,
@@ -755,7 +874,7 @@ impl CheckKind {
CheckKind::NewLineAfterLastParagraph => &CheckCode::D209,
CheckKind::NewLineAfterSectionName(_) => &CheckCode::D406,
CheckKind::NoBlankLineAfterFunction(_) => &CheckCode::D202,
CheckKind::NoBlankLineAfterSummary => &CheckCode::D205,
CheckKind::BlankLineAfterSummary => &CheckCode::D205,
CheckKind::NoBlankLineBeforeClass(_) => &CheckCode::D211,
CheckKind::NoBlankLineBeforeFunction(_) => &CheckCode::D201,
CheckKind::NoBlankLinesBetweenHeaderAndContent(_) => &CheckCode::D412,
@@ -788,6 +907,16 @@ impl CheckKind {
CheckKind::InvalidArgumentName(_) => &CheckCode::N803,
CheckKind::InvalidFirstArgumentNameForClassMethod => &CheckCode::N804,
CheckKind::InvalidFirstArgumentNameForMethod => &CheckCode::N805,
CheckKind::NonLowercaseVariableInFunction(..) => &CheckCode::N806,
CheckKind::DunderFunctionName => &CheckCode::N807,
CheckKind::ConstantImportedAsNonConstant(..) => &CheckCode::N811,
CheckKind::LowercaseImportedAsNonLowercase(..) => &CheckCode::N812,
CheckKind::CamelcaseImportedAsLowercase(..) => &CheckCode::N813,
CheckKind::CamelcaseImportedAsConstant(..) => &CheckCode::N814,
CheckKind::MixedCaseVariableInClassScope(..) => &CheckCode::N815,
CheckKind::MixedCaseVariableInGlobalScope(..) => &CheckCode::N816,
CheckKind::CamelcaseImportedAsAcronym(..) => &CheckCode::N817,
CheckKind::ErrorSuffixOnExceptionName(..) => &CheckCode::N818,
// Meta
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
}
@@ -914,18 +1043,23 @@ impl CheckKind {
CheckKind::UndefinedName(name) => {
format!("Undefined name `{name}`")
}
CheckKind::UnusedImport(names) => {
CheckKind::UnusedImport(names, in_init_py) => {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
format!("{names} imported but unused")
if *in_init_py {
format!("{names} imported but unused and missing from `__all__`")
} else {
format!("{names} imported but unused")
}
}
CheckKind::UnusedVariable(name) => {
format!("Local variable `{name}` is assigned to but never used")
}
CheckKind::YieldOutsideFunction => {
"`yield` or `yield from` statement outside of a function/method".to_string()
"`yield` or `yield from` statement outside of a function".to_string()
}
// pycodestyle warnings
CheckKind::NoNewLineAtEndOfFile => "No newline at end of file".to_string(),
CheckKind::InvalidEscapeSequence(char) => format!("Invalid escape sequence: '\\{char}'"),
// flake8-builtins
CheckKind::BuiltinVariableShadowing(name) => {
format!("Variable `{name}` is shadowing a python builtin")
@@ -937,10 +1071,16 @@ impl CheckKind {
format!("Class attribute `{name}` is shadowing a python builtin")
}
// flake8-bugbear
CheckKind::UnaryPrefixIncrement => "Python does not support the unary prefix increment. Writing `++n` is equivalent to `+(+(n))`, which equals `n`. You meant `n += 1`.".to_string(),
CheckKind::MutableArgumentDefault => "Do not use mutable data structures for argument defaults.".to_string(),
CheckKind::UnusedLoopControlVariable(name) => format!("Loop control variable `{name}` not used within the loop body. If this is intended, start the name with an underscore."),
CheckKind::DoNotAssertFalse => {
"Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`"
.to_string()
}
CheckKind::RedundantTupleInExceptionHandler(name) => {
format!("A length-one tuple literal is redundant. Write `except {name}:` instead of `except ({name},):`.")
}
CheckKind::DuplicateHandlerException(names) => {
if names.len() == 1 {
let name = &names[0];
@@ -950,6 +1090,9 @@ impl CheckKind {
format!("Exception handler with duplicate exceptions: {names}")
}
}
CheckKind::NoAssertRaisesException => {
"`assertRaises(Exception):` should be considered evil. It can lead to your test passing even if the code being tested is never executed due to a typo. Either assert for a more specific exception (builtin or custom), use `assertRaisesRegex`, or use the context manager form of `assertRaises`.".to_string()
}
CheckKind::DuplicateTryBlockException(name) => {
format!("try-except block with duplicate exception `{name}`")
}
@@ -1013,7 +1156,7 @@ impl CheckKind {
format!("Unnecessary subscript reversal of iterable within `{func}()`")
}
CheckKind::UnnecessaryComprehension(obj_type) => {
format!(" Unnecessary `{obj_type}` comprehension (rewrite using `{obj_type}()`)")
format!("Unnecessary `{obj_type}` comprehension (rewrite using `{obj_type}()`)")
}
CheckKind::UnnecessaryMap(obj_type) => {
if obj_type == "generator" {
@@ -1025,6 +1168,26 @@ impl CheckKind {
// flake8-print
CheckKind::PrintFound => "`print` found".to_string(),
CheckKind::PPrintFound => "`pprint` found".to_string(),
// flake8-quotes
CheckKind::BadQuotesInlineString(quote) => {
match quote {
Quote::Single => "Double quotes found but single quotes preferred".to_string(),
Quote::Double => "Single quotes found but double quotes preferred".to_string(),
}
},
CheckKind::BadQuotesMultilineString(quote) => {
match quote {
Quote::Single => "Double quote multiline found but single quotes preferred".to_string(),
Quote::Double => "Single quote multiline found but double quotes preferred".to_string(),
}
},
CheckKind::BadQuotesDocstring(quote) => {
match quote {
Quote::Single => "Double quote docstring found but single quotes preferred".to_string(),
Quote::Double => "Single quote docstring found but double quotes preferred".to_string(),
}
},
CheckKind::AvoidQuoteEscape => "Change outer quotes to avoid escaping inner quotes".to_string(),
// pyupgrade
CheckKind::TypeOfPrimitive(primitive) => {
format!("Use `{}` instead of `type(...)`", primitive.builtin())
@@ -1034,7 +1197,7 @@ impl CheckKind {
}
CheckKind::UselessMetaclassType => "`__metaclass__ = type` is implied".to_string(),
CheckKind::DeprecatedUnittestAlias(alias, target) => {
format!("`{}` is deprecated, use `{}` instead", alias, target)
format!("`{alias}` is deprecated, use `{target}` instead")
}
CheckKind::UselessObjectInheritance(name) => {
format!("Class `{name}` inherits from object")
@@ -1052,7 +1215,7 @@ impl CheckKind {
}
// pydocstyle
CheckKind::FitsOnOneLine => "One-line docstring should fit on one line".to_string(),
CheckKind::NoBlankLineAfterSummary => {
CheckKind::BlankLineAfterSummary => {
"1 blank line required between summary line and description".to_string()
}
CheckKind::NewLineAfterLastParagraph => {
@@ -1078,7 +1241,7 @@ impl CheckKind {
"Multi-line docstring summary should start at the second line".to_string()
}
CheckKind::NoSignature => {
"First line should not be the function's 'signature'".to_string()
"First line should not be the function's signature".to_string()
}
CheckKind::NoBlankLineBeforeFunction(num_lines) => {
format!("No blank lines allowed before function docstring (found {num_lines})")
@@ -1104,10 +1267,10 @@ impl CheckKind {
CheckKind::PublicNestedClass => "Missing docstring in public nested class".to_string(),
CheckKind::PublicInit => "Missing docstring in `__init__`".to_string(),
CheckKind::NoThisPrefix => {
"First word of the docstring should not be `This`".to_string()
"First word of the docstring should not be 'This'".to_string()
}
CheckKind::SkipDocstring => {
"Function decorated with @overload shouldn't contain a docstring".to_string()
"Function decorated with `@overload` shouldn't contain a docstring".to_string()
}
CheckKind::CapitalizeSectionName(name) => {
format!("Section name should be properly capitalized (\"{name}\")")
@@ -1178,6 +1341,36 @@ impl CheckKind {
CheckKind::InvalidFirstArgumentNameForMethod => {
"First argument of a method should be named `self`".to_string()
}
CheckKind::NonLowercaseVariableInFunction(name) => {
format!("Variable `{name}` in function should be lowercase")
}
CheckKind::DunderFunctionName => {
"Function name should not start and end with `__`".to_string()
}
CheckKind::ConstantImportedAsNonConstant(name, asname) => {
format!("Constant `{name}` imported as non-constant `{asname}`")
}
CheckKind::LowercaseImportedAsNonLowercase(name, asname) => {
format!("Lowercase `{name}` imported as non-lowercase `{asname}`")
}
CheckKind::CamelcaseImportedAsLowercase(name, asname) => {
format!("Camelcase `{name}` imported as lowercase `{asname}`")
}
CheckKind::CamelcaseImportedAsConstant(name, asname) => {
format!("Camelcase `{name}` imported as constant `{asname}`")
}
CheckKind::MixedCaseVariableInClassScope(name) => {
format!("Variable `{name}` in class scope should not be mixedCase")
}
CheckKind::MixedCaseVariableInGlobalScope(name) => {
format!("Variable `{name}` in global scope should not be mixedCase")
}
CheckKind::CamelcaseImportedAsAcronym(name, asname) => {
format!("Camelcase `{name}` imported as acronym `{asname}`")
}
CheckKind::ErrorSuffixOnExceptionName(name) => {
format!("Exception name `{name}` should be named with an Error suffix")
}
// Meta
CheckKind::UnusedNOQA(codes) => match codes {
None => "Unused `noqa` directive".to_string(),
@@ -1198,28 +1391,58 @@ impl CheckKind {
}
}
/// The summary text for the check. Typically a truncated form of the body text.
pub fn summary(&self) -> String {
match self {
CheckKind::UnaryPrefixIncrement => {
"Python does not support the unary prefix increment.".to_string()
}
CheckKind::UnusedLoopControlVariable(name) => {
format!("Loop control variable `{name}` not used within the loop body.")
}
CheckKind::NoAssertRaisesException => {
"`assertRaises(Exception):` should be considered evil.".to_string()
}
_ => self.body(),
}
}
/// Whether the check kind is (potentially) fixable.
pub fn fixable(&self) -> bool {
matches!(
self,
|CheckKind::BlankLineAfterLastSection(_)| CheckKind::BlankLineAfterSection(_)
CheckKind::BlankLineAfterLastSection(_)
| CheckKind::BlankLineAfterSection(_)
| CheckKind::BlankLineAfterSummary
| CheckKind::BlankLineBeforeSection(_)
| CheckKind::CapitalizeSectionName(_)
| CheckKind::DashedUnderlineAfterSection(_)
| CheckKind::DeprecatedUnittestAlias(_, _)
| CheckKind::DoNotAssertFalse
| CheckKind::DuplicateHandlerException(_)
| CheckKind::NewLineAfterLastParagraph
| CheckKind::NewLineAfterSectionName(_)
| CheckKind::NoBlankLineAfterFunction(_)
| CheckKind::NoBlankLineAfterSummary
| CheckKind::NoBlankLineBeforeClass(_)
| CheckKind::NoBlankLineBeforeFunction(_)
| CheckKind::NoBlankLinesBetweenHeaderAndContent(_)
| CheckKind::NoOverIndentation
| CheckKind::NoSurroundingWhitespace
| CheckKind::NoUnderIndentation
| CheckKind::OneBlankLineAfterClass(_)
| CheckKind::OneBlankLineBeforeClass(_)
| CheckKind::PPrintFound
| CheckKind::PrintFound
| CheckKind::SectionNameEndsInColon(_)
| CheckKind::SectionNotOverIndented(_)
| CheckKind::SectionUnderlineAfterName(_)
| CheckKind::SectionUnderlineMatchesSectionLength(_)
| CheckKind::SectionUnderlineNotOverIndented(_)
| CheckKind::SuperCallWithParameters
| CheckKind::TypeOfPrimitive(_)
| CheckKind::UnnecessaryAbspath
| CheckKind::UnusedImport(_)
| CheckKind::UnusedImport(_, false)
| CheckKind::UnusedLoopControlVariable(_)
| CheckKind::UnusedNOQA(_)
| CheckKind::UsePEP585Annotation(_)
| CheckKind::UsePEP604Annotation
@@ -1229,43 +1452,6 @@ impl CheckKind {
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fix {
pub content: String,
pub location: Location,
pub end_location: Location,
pub applied: bool,
}
impl Fix {
pub fn deletion(start: Location, end: Location) -> Self {
Self {
content: "".to_string(),
location: start,
end_location: end,
applied: false,
}
}
pub fn replacement(content: String, start: Location, end: Location) -> Self {
Self {
content,
location: start,
end_location: end,
applied: false,
}
}
pub fn insertion(content: String, at: Location) -> Self {
Self {
content,
location: at,
end_location: at,
applied: false,
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Check {
pub kind: CheckKind,
@@ -1275,11 +1461,11 @@ pub struct Check {
}
impl Check {
pub fn new(kind: CheckKind, rage: Range) -> Self {
pub fn new(kind: CheckKind, range: Range) -> Self {
Self {
kind,
location: rage.location,
end_location: rage.end_location,
location: range.location,
end_location: range.end_location,
fix: None,
}
}

1106
src/checks_gen.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,11 @@ use clap::{command, Parser};
use log::warn;
use regex::Regex;
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::printer::SerializationFormat;
use crate::pyproject::StrCheckCodePair;
use crate::settings::PythonVersion;
use crate::RawSettings;
use crate::settings::configuration::Configuration;
use crate::settings::types::PythonVersion;
use crate::settings::types::StrCheckCodePair;
#[derive(Debug, Parser)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
@@ -21,11 +21,14 @@ pub struct Cli {
#[arg(long)]
pub config: Option<PathBuf>,
/// Enable verbose logging.
#[arg(short, long)]
#[arg(short, long, group = "verbosity")]
pub verbose: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[arg(short, long)]
/// Only log errors.
#[arg(short, long, group = "verbosity")]
pub quiet: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[arg(short, long, group = "verbosity")]
pub silent: bool,
/// Exit with status code "0", even upon detecting errors.
#[arg(short, long)]
pub exit_zero: bool,
@@ -40,16 +43,16 @@ pub struct Cli {
pub no_cache: bool,
/// List of error codes to enable.
#[arg(long, value_delimiter = ',')]
pub select: Vec<CheckCode>,
pub select: Vec<CheckCodePrefix>,
/// Like --select, but adds additional error codes on top of the selected ones.
#[arg(long, value_delimiter = ',')]
pub extend_select: Vec<CheckCode>,
pub extend_select: Vec<CheckCodePrefix>,
/// List of error codes to ignore.
#[arg(long, value_delimiter = ',')]
pub ignore: Vec<CheckCode>,
pub ignore: Vec<CheckCodePrefix>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[arg(long, value_delimiter = ',')]
pub extend_ignore: Vec<CheckCode>,
pub extend_ignore: Vec<CheckCodePrefix>,
/// List of paths, used to exclude files and/or directories from checks.
#[arg(long, value_delimiter = ',')]
pub exclude: Vec<String>,
@@ -103,10 +106,10 @@ impl fmt::Display for Warnable {
/// Warn the user if they attempt to enable a code that won't be respected.
pub fn warn_on(
flag: Warnable,
codes: &[CheckCode],
cli_ignore: &[CheckCode],
cli_extend_ignore: &[CheckCode],
pyproject_settings: &RawSettings,
codes: &[CheckCodePrefix],
cli_ignore: &[CheckCodePrefix],
cli_extend_ignore: &[CheckCodePrefix],
pyproject_configuration: &Configuration,
pyproject_path: &Option<PathBuf>,
) {
for code in codes {
@@ -114,7 +117,7 @@ pub fn warn_on(
if cli_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --ignore")
}
} else if pyproject_settings.ignore.contains(code) {
} else if pyproject_configuration.ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `ignore` field in {}",
@@ -128,7 +131,7 @@ pub fn warn_on(
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) {
} else if pyproject_configuration.extend_ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `extend_ignore` field in {}",

42
src/cst/helpers.rs Normal file
View File

@@ -0,0 +1,42 @@
use libcst_native::{Expression, NameOrAttribute};
fn compose_call_path_inner<'a>(expr: &'a Expression, parts: &mut Vec<&'a str>) {
match &expr {
Expression::Call(expr) => {
compose_call_path_inner(&expr.func, parts);
}
Expression::Attribute(expr) => {
compose_call_path_inner(&expr.value, parts);
parts.push(expr.attr.value);
}
Expression::Name(expr) => {
parts.push(expr.value);
}
_ => {}
}
}
pub fn compose_call_path(expr: &Expression) -> Option<String> {
let mut segments = vec![];
compose_call_path_inner(expr, &mut segments);
if segments.is_empty() {
None
} else {
Some(segments.join("."))
}
}
pub fn compose_module_path(module: &NameOrAttribute) -> String {
match module {
NameOrAttribute::N(name) => name.value.to_string(),
NameOrAttribute::A(attr) => {
let name = attr.attr.value;
let prefix = compose_call_path(&attr.value);
if let Some(prefix) = prefix {
format!("{prefix}.{name}")
} else {
name.to_string()
}
}
}
}

1
src/cst/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod helpers;

View File

@@ -1,8 +0,0 @@
pub mod definition;
pub mod extraction;
mod google;
mod helpers;
mod numpy;
pub mod plugins;
pub mod sections;
mod styles;

View File

@@ -2,16 +2,7 @@
use std::collections::BTreeSet;
use crate::ast::types::Range;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::definition::Definition;
use crate::docstrings::sections;
use crate::docstrings::sections::SectionContext;
use crate::docstrings::styles::SectionStyle;
pub(crate) static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
@@ -78,73 +69,3 @@ pub(crate) static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> =
"yields",
])
});
// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`.
static GOOGLE_ARGS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s*(\w+)\s*(\(.*?\))?\s*:\n?\s*.+").expect("Invalid regex"));
fn check_args_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) {
let mut args_sections: Vec<String> = vec![];
for line in textwrap::dedent(&context.following_lines.join("\n")).lines() {
if line
.chars()
.next()
.map(|char| char.is_whitespace())
.unwrap_or(true)
{
// This is a continuation of documentation for the last
// parameter because it does start with whitespace.
if let Some(current) = args_sections.last_mut() {
current.push_str(line);
}
} else {
// This line is the start of documentation for the next
// parameter because it doesn't start with any whitespace.
args_sections.push(line.to_string());
}
}
sections::check_missing_args(
checker,
definition,
// Collect the list of arguments documented in the docstring.
&BTreeSet::from_iter(args_sections.iter().filter_map(|section| {
match GOOGLE_ARGS_REGEX.captures(section.as_str()) {
Some(caps) => caps.get(1).map(|arg_name| arg_name.as_str()),
None => None,
}
})),
)
}
pub(crate) fn check_google_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
sections::check_common_section(checker, definition, context, &SectionStyle::Google);
if checker.settings.enabled.contains(&CheckCode::D416) {
let suffix = context
.line
.trim()
.strip_prefix(&context.section_name)
.unwrap();
if suffix != ":" {
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
checker.add_check(Check::new(
CheckKind::SectionNameEndsInColon(context.section_name.to_string()),
Range::from_located(docstring),
))
}
}
if checker.settings.enabled.contains(&CheckCode::D417) {
let capitalized_section_name = titlecase::titlecase(&context.section_name);
if capitalized_section_name == "Args" || capitalized_section_name == "Arguments" {
check_args_section(checker, definition, context);
}
}
}

View File

@@ -25,10 +25,18 @@ pub fn leading_space(line: &str) -> String {
}
/// Extract the leading indentation from a docstring.
pub fn indentation<'a>(checker: &'a mut Checker, docstring: &Expr) -> &'a str {
pub fn indentation<'a>(checker: &'a Checker, docstring: &Expr) -> &'a str {
let range = Range::from_located(docstring);
checker.locator.slice_source_code_range(&Range {
location: Location::new(range.location.row(), 1),
location: Location::new(range.location.row(), 0),
end_location: Location::new(range.location.row(), range.location.column()),
})
}
/// Replace any non-whitespace characters from an indentation string.
pub fn clean(indentation: &str) -> String {
indentation
.chars()
.map(|char| if char.is_whitespace() { char } else { ' ' })
.collect()
}

7
src/docstrings/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod definition;
pub mod extraction;
pub mod google;
pub mod helpers;
pub mod numpy;
pub mod sections;
pub mod styles;

View File

@@ -2,16 +2,8 @@
use std::collections::BTreeSet;
use crate::ast::types::Range;
use once_cell::sync::Lazy;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::definition::Definition;
use crate::docstrings::sections::SectionContext;
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::{helpers, sections};
pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"short summary",
@@ -47,68 +39,3 @@ pub(crate) static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(
"Methods",
])
});
fn check_parameters_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
// Collect the list of arguments documented in the docstring.
let mut docstring_args: BTreeSet<&str> = Default::default();
let section_level_indent = helpers::leading_space(context.line);
for i in 1..context.following_lines.len() {
let current_line = context.following_lines[i - 1];
let current_leading_space = helpers::leading_space(current_line);
let next_line = context.following_lines[i];
if current_leading_space == section_level_indent
&& (helpers::leading_space(next_line).len() > current_leading_space.len())
&& !next_line.trim().is_empty()
{
let parameters = if let Some(semi_index) = current_line.find(':') {
// If the parameter has a type annotation, exclude it.
&current_line[..semi_index]
} else {
// Otherwise, it's just a list of parameters on the current line.
current_line.trim()
};
// Notably, NumPy lets you put multiple parameters of the same type on the same line.
for parameter in parameters.split(',') {
docstring_args.insert(parameter.trim());
}
}
}
// Validate that all arguments were documented.
sections::check_missing_args(checker, definition, &docstring_args);
}
pub(crate) fn check_numpy_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
sections::check_common_section(checker, definition, context, &SectionStyle::NumPy);
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::from_located(docstring),
))
}
}
if checker.settings.enabled.contains(&CheckCode::D417) {
let capitalized_section_name = titlecase::titlecase(&context.section_name);
if capitalized_section_name == "Parameters" {
check_parameters_section(checker, definition, context);
}
}
}

View File

@@ -1,826 +0,0 @@
//! Abstractions for tracking and validating docstrings in Python code.
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Constant, ExprKind, Location, StmtKind};
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::docstrings::google::check_google_section;
use crate::docstrings::helpers::{
indentation, leading_space, SINGLE_QUOTE_PREFIXES, TRIPLE_QUOTE_PREFIXES,
};
use crate::docstrings::numpy::check_numpy_section;
use crate::docstrings::sections::section_contexts;
use crate::docstrings::styles::SectionStyle;
use crate::visibility::{is_init, is_magic, is_overload, Visibility};
/// 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::from_located(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::from_located(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 {
let mut check = Check::new(
CheckKind::NoBlankLineBeforeFunction(blank_lines_before),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::deletion(
Location::new(docstring.location.row() - blank_lines_before, 1),
Location::new(docstring.location.row(), 1),
));
}
checker.add_check(check);
}
}
if checker.settings.enabled.contains(&CheckCode::D202) {
let all_blank_after = after
.lines()
.skip(1)
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
if all_blank_after {
return;
}
let blank_lines_after = after
.lines()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
// 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.
let expected_blank_lines_after =
if INNER_FUNCTION_OR_CLASS_REGEX.is_match(after) {
1
} else {
0
};
if blank_lines_after != expected_blank_lines_after {
let mut check = Check::new(
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::deletion(
Location::new(
docstring.location.row() + 1 + expected_blank_lines_after,
1,
),
Location::new(docstring.location.row() + 1 + blank_lines_after, 1),
));
}
checker.add_check(check);
}
}
}
}
}
}
/// 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::from_located(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 checker.settings.enabled.contains(&CheckCode::D211) {
if blank_lines_before != 0 {
let mut check = Check::new(
CheckKind::NoBlankLineBeforeClass(blank_lines_before),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
{
check.amend(Fix::deletion(
Location::new(docstring.location.row() - blank_lines_before, 1),
Location::new(docstring.location.row(), 1),
));
}
checker.add_check(check);
}
}
if checker.settings.enabled.contains(&CheckCode::D203) {
if blank_lines_before != 1 {
let mut check = Check::new(
CheckKind::OneBlankLineBeforeClass(blank_lines_before),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
{
check.amend(Fix::replacement(
"\n".to_string(),
Location::new(docstring.location.row() - blank_lines_before, 1),
Location::new(docstring.location.row(), 1),
));
}
checker.add_check(check);
}
}
}
if checker.settings.enabled.contains(&CheckCode::D204) {
let all_blank_after = after
.lines()
.skip(1)
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
if all_blank_after {
return;
}
let blank_lines_after = after
.lines()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
if blank_lines_after != 1 {
let mut check = Check::new(
CheckKind::OneBlankLineAfterClass(blank_lines_after),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::replacement(
"\n".to_string(),
Location::new(docstring.end_location.row() + 1, 1),
Location::new(
docstring.end_location.row() + 1 + blank_lines_after,
1,
),
));
}
checker.add_check(check);
}
}
}
}
}
}
/// 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 {
let mut check = Check::new(
CheckKind::NoBlankLineAfterSummary,
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::replacement(
"\n".to_string(),
Location::new(docstring.location.row() + 1, 1),
Location::new(docstring.location.row() + 1 + blanks_count, 1),
));
}
checker.add_check(check);
}
}
}
}
/// D206, D207, D208
pub fn indent(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() <= 1 {
return;
}
let mut has_seen_tab = false;
let mut has_seen_over_indent = false;
let mut has_seen_under_indent = false;
let docstring_indent = indentation(checker, docstring).to_string();
if !has_seen_tab {
if docstring_indent.contains('\t') {
if checker.settings.enabled.contains(&CheckCode::D206) {
checker.add_check(Check::new(
CheckKind::IndentWithSpaces,
Range::from_located(docstring),
));
}
has_seen_tab = true;
}
}
for i in 0..lines.len() {
// First lines and continuations doesn't need any indentation.
if i == 0 || lines[i - 1].ends_with('\\') {
continue;
}
// Omit empty lines, except for the last line, which is non-empty by way of
// containing the closing quotation marks.
if i < lines.len() - 1 && lines[i].trim().is_empty() {
continue;
}
let line_indent = leading_space(lines[i]);
if !has_seen_tab {
if line_indent.contains('\t') {
if checker.settings.enabled.contains(&CheckCode::D206) {
checker.add_check(Check::new(
CheckKind::IndentWithSpaces,
Range::from_located(docstring),
));
}
has_seen_tab = true;
}
}
if !has_seen_over_indent {
if line_indent.len() > docstring_indent.len() {
if checker.settings.enabled.contains(&CheckCode::D208) {
checker.add_check(Check::new(
CheckKind::NoOverIndentation,
Range::from_located(docstring),
));
}
has_seen_over_indent = true;
}
}
if !has_seen_under_indent {
if line_indent.len() < docstring_indent.len() {
if checker.settings.enabled.contains(&CheckCode::D207) {
checker.add_check(Check::new(
CheckKind::NoUnderIndentation,
Range::from_located(docstring),
));
}
has_seen_under_indent = true;
}
}
}
}
}
}
/// 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::from_located(docstring));
if let Some(last_line) = content.lines().last().map(|line| line.trim()) {
if last_line != "\"\"\"" && last_line != "'''" {
let mut check = Check::new(
CheckKind::NewLineAfterLastParagraph,
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
{
// Insert a newline just before the end-quote(s).
let mut content = "\n".to_string();
content.push_str(indentation(checker, docstring));
check.amend(Fix::insertion(
content,
Location::new(
docstring.end_location.row(),
docstring.end_location.column() - "\"\"\"".len(),
),
));
}
checker.add_check(check);
}
}
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() {
let trimmed = line.trim();
if trimmed.is_empty() {
return;
}
if line != trimmed {
let mut check = Check::new(
CheckKind::NoSurroundingWhitespace,
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(first_line) = checker
.locator
.slice_source_code_range(&Range::from_located(docstring))
.lines()
.next()
.map(|line| line.to_lowercase())
{
for pattern in TRIPLE_QUOTE_PREFIXES.iter().chain(SINGLE_QUOTE_PREFIXES)
{
if first_line.starts_with(pattern) {
check.amend(Fix::replacement(
trimmed.to_string(),
Location::new(
docstring.location.row(),
docstring.location.column() + pattern.len(),
),
Location::new(
docstring.location.row(),
docstring.location.column()
+ pattern.len()
+ line.chars().count(),
),
));
break;
}
}
}
}
checker.add_check(check);
}
}
}
}
}
/// 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() {
if let Some(first_line) = checker
.locator
.slice_source_code_range(&Range::from_located(docstring))
.lines()
.next()
.map(|line| line.to_lowercase())
{
if TRIPLE_QUOTE_PREFIXES.contains(&first_line.as_str()) {
if checker.settings.enabled.contains(&CheckCode::D212) {
checker.add_check(Check::new(
CheckKind::MultiLineSummaryFirstLine,
Range::from_located(docstring),
));
}
} else {
if checker.settings.enabled.contains(&CheckCode::D213) {
checker.add_check(Check::new(
CheckKind::MultiLineSummarySecondLine,
Range::from_located(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
{
if let Some(first_line) = checker
.locator
.slice_source_code_range(&Range::from_located(docstring))
.lines()
.next()
.map(|line| line.to_lowercase())
{
let starts_with_triple = if string.contains("\"\"\"") {
first_line.starts_with("'''")
|| first_line.starts_with("u'''")
|| first_line.starts_with("r'''")
|| first_line.starts_with("ur'''")
} else {
first_line.starts_with("\"\"\"")
|| first_line.starts_with("u\"\"\"")
|| first_line.starts_with("r\"\"\"")
|| first_line.starts_with("ur\"\"\"")
};
if !starts_with_triple {
checker.add_check(Check::new(
CheckKind::UsesTripleQuotes,
Range::from_located(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::from_located(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::from_located(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::from_located(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::from_located(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::from_located(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::from_located(docstring),
));
}
return false;
}
}
}
true
}
/// D212, D214, D215, D405, D406, D407, D408, D409, D410, D411, D412, D413, D414, D416, D417
pub fn 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;
}
// First, interpret as NumPy-style sections.
let mut found_numpy_section = false;
for context in &section_contexts(&lines, &SectionStyle::NumPy) {
found_numpy_section = true;
check_numpy_section(checker, definition, context);
}
// If no such sections were identified, interpret as Google-style sections.
if !found_numpy_section {
for context in &section_contexts(&lines, &SectionStyle::Google) {
check_google_section(checker, definition, context);
}
}
}
}
}

View File

@@ -1,17 +1,5 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustpython_ast::{Arg, Location, StmtKind};
use titlecase::titlecase;
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::docstrings::helpers;
use crate::docstrings::styles::SectionStyle;
use crate::visibility::is_static;
#[derive(Debug)]
pub(crate) struct SectionContext<'a> {
@@ -20,7 +8,7 @@ pub(crate) struct SectionContext<'a> {
pub(crate) line: &'a str,
pub(crate) following_lines: &'a [&'a str],
pub(crate) is_last_section: bool,
original_index: usize,
pub(crate) original_index: usize,
}
fn suspected_as_section(line: &str, style: &SectionStyle) -> bool {
@@ -109,295 +97,3 @@ pub(crate) fn section_contexts<'a>(
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() {
if checker.settings.enabled.contains(&CheckCode::D407) {
checker.add_check(Check::new(
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
Range::from_located(docstring),
));
}
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
Range::from_located(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 {
if checker.settings.enabled.contains(&CheckCode::D407) {
checker.add_check(Check::new(
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
Range::from_located(docstring),
));
}
if blank_lines_after_header > 0 {
if checker.settings.enabled.contains(&CheckCode::D212) {
checker.add_check(Check::new(
CheckKind::NoBlankLinesBetweenHeaderAndContent(
context.section_name.to_string(),
),
Range::from_located(docstring),
));
}
}
} else {
if blank_lines_after_header > 0 {
if checker.settings.enabled.contains(&CheckCode::D408) {
checker.add_check(Check::new(
CheckKind::SectionUnderlineAfterName(context.section_name.to_string()),
Range::from_located(docstring),
));
}
}
if non_empty_line
.trim()
.chars()
.filter(|char| *char == '-')
.count()
!= context.section_name.len()
{
if checker.settings.enabled.contains(&CheckCode::D409) {
checker.add_check(Check::new(
CheckKind::SectionUnderlineMatchesSectionLength(
context.section_name.to_string(),
),
Range::from_located(docstring),
));
}
}
if checker.settings.enabled.contains(&CheckCode::D215) {
if helpers::leading_space(non_empty_line).len()
> helpers::indentation(checker, docstring).len()
{
checker.add_check(Check::new(
CheckKind::SectionUnderlineNotOverIndented(context.section_name.to_string()),
Range::from_located(docstring),
));
}
}
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()) {
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
Range::from_located(docstring),
));
}
} else {
if checker.settings.enabled.contains(&CheckCode::D412) {
checker.add_check(Check::new(
CheckKind::NoBlankLinesBetweenHeaderAndContent(
context.section_name.to_string(),
),
Range::from_located(docstring),
));
}
}
}
} else {
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
Range::from_located(docstring),
));
}
}
}
}
pub(crate) fn check_common_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
style: &SectionStyle,
) {
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
if checker.settings.enabled.contains(&CheckCode::D405) {
if !style
.section_names()
.contains(&context.section_name.as_str())
&& style
.section_names()
.contains(titlecase(&context.section_name).as_str())
{
checker.add_check(Check::new(
CheckKind::CapitalizeSectionName(context.section_name.to_string()),
Range::from_located(docstring),
))
}
}
if checker.settings.enabled.contains(&CheckCode::D214) {
if helpers::leading_space(context.line).len()
> helpers::indentation(checker, docstring).len()
{
checker.add_check(Check::new(
CheckKind::SectionNotOverIndented(context.section_name.to_string()),
Range::from_located(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) {
let mut check = Check::new(
CheckKind::BlankLineAfterLastSection(context.section_name.to_string()),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::insertion(
"\n".to_string(),
Location::new(
docstring.location.row()
+ context.original_index
+ 1
+ context.following_lines.len(),
1,
),
))
}
checker.add_check(check);
}
} else {
if checker.settings.enabled.contains(&CheckCode::D410) {
let mut check = Check::new(
CheckKind::BlankLineAfterSection(context.section_name.to_string()),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::insertion(
"\n".to_string(),
Location::new(
docstring.location.row()
+ context.original_index
+ 1
+ context.following_lines.len(),
1,
),
))
}
checker.add_check(check);
}
}
}
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::from_located(docstring),
))
}
}
check_blanks_and_section_underline(checker, definition, context);
}
pub(crate) fn check_missing_args(
checker: &mut Checker,
definition: &Definition,
docstrings_args: &BTreeSet<&str>,
) {
if let DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent)
| DefinitionKind::Method(parent) = definition.kind
{
if let StmtKind::FunctionDef {
args: arguments, ..
}
| StmtKind::AsyncFunctionDef {
args: arguments, ..
} = &parent.node
{
// Collect all the arguments into a single vector.
let mut all_arguments: Vec<&Arg> = arguments
.args
.iter()
.chain(arguments.posonlyargs.iter())
.chain(arguments.kwonlyargs.iter())
.skip(
// If this is a non-static method, skip `cls` or `self`.
if matches!(definition.kind, DefinitionKind::Method(_)) && !is_static(parent) {
1
} else {
0
},
)
.collect();
if let Some(arg) = &arguments.vararg {
all_arguments.push(arg);
}
if let Some(arg) = &arguments.kwarg {
all_arguments.push(arg);
}
// Look for arguments that weren't included in the docstring.
let mut missing_args: BTreeSet<&str> = Default::default();
for arg in all_arguments {
let arg_name = arg.node.arg.as_str();
if arg_name.starts_with('_') {
continue;
}
if docstrings_args.contains(&arg_name) {
continue;
}
missing_args.insert(arg_name);
}
if !missing_args.is_empty() {
let names = missing_args
.into_iter()
.map(String::from)
.sorted()
.collect();
checker.add_check(Check::new(
CheckKind::DocumentAllArguments(names),
Range::from_located(parent),
));
}
}
}
}

View File

@@ -0,0 +1 @@
pub mod plugins;

View File

@@ -1,9 +1,9 @@
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind, Fix};
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
fn assertion_error(msg: &Option<Box<Expr>>) -> Stmt {
@@ -43,16 +43,15 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: &Optio
} = &test.node
{
let mut check = Check::new(CheckKind::DoNotAssertFalse, Range::from_located(test));
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if checker.patch() {
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_stmt(&assertion_error(msg)) {
if let Ok(content) = generator.generate() {
check.amend(Fix {
check.amend(Fix::replacement(
content,
location: stmt.location,
end_location: stmt.end_location,
applied: false,
})
stmt.location,
stmt.end_location.unwrap(),
));
}
}
}

View File

@@ -0,0 +1,25 @@
use rustpython_ast::{ExprKind, Stmt, Withitem};
use crate::ast::helpers::match_name_or_attr;
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// B017
pub fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[Withitem]) {
if let Some(item) = items.first() {
let item_context = &item.context_expr;
if let ExprKind::Call { func, args, .. } = &item_context.node {
if match_name_or_attr(func, "assertRaises")
&& args.len() == 1
&& match_name_or_attr(args.first().unwrap(), "Exception")
&& item.optional_vars.is_none()
{
checker.add_check(Check::new(
CheckKind::NoAssertRaisesException,
Range::from_located(stmt),
));
}
}
}
}

View File

@@ -5,9 +5,9 @@ use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKi
use crate::ast::helpers;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::fixer;
use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::code_gen::SourceGenerator;
fn type_pattern(elts: Vec<&Expr>) -> Expr {
@@ -49,17 +49,16 @@ pub fn duplicate_handler_exceptions(
),
checker.locate_check(Range::from_located(expr)),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if checker.patch() {
// 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 {
check.amend(Fix::replacement(
content,
location: expr.location,
end_location: expr.end_location,
applied: false,
})
expr.location,
expr.end_location.unwrap(),
))
}
}
}

View File

@@ -0,0 +1,16 @@
pub use assert_false::assert_false;
pub use assert_raises_exception::assert_raises_exception;
pub use duplicate_exceptions::duplicate_exceptions;
pub use duplicate_exceptions::duplicate_handler_exceptions;
pub use mutable_argument_default::mutable_argument_default;
pub use redundant_tuple_in_exception_handler::redundant_tuple_in_exception_handler;
pub use unary_prefix_increment::unary_prefix_increment;
pub use unused_loop_control_variable::unused_loop_control_variable;
mod assert_false;
mod assert_raises_exception;
mod duplicate_exceptions;
mod mutable_argument_default;
mod redundant_tuple_in_exception_handler;
mod unary_prefix_increment;
mod unused_loop_control_variable;

View File

@@ -0,0 +1,62 @@
use rustpython_ast::{Arguments, ExprKind};
use crate::ast::types::{CheckLocator, Range};
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// B006
pub fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) {
for expr in arguments
.defaults
.iter()
.chain(arguments.kw_defaults.iter())
{
match &expr.node {
ExprKind::List { .. }
| ExprKind::Dict { .. }
| ExprKind::Set { .. }
| ExprKind::ListComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::SetComp { .. } => {
checker.add_check(Check::new(
CheckKind::MutableArgumentDefault,
checker.locate_check(Range::from_located(expr)),
));
}
ExprKind::Call { func, .. } => match &func.node {
ExprKind::Name { id, .. }
if id == "dict"
|| id == "list"
|| id == "set"
|| id == "Counter"
|| id == "OrderedDict"
|| id == "defaultdict"
|| id == "deque" =>
{
checker.add_check(Check::new(
CheckKind::MutableArgumentDefault,
checker.locate_check(Range::from_located(expr)),
));
}
ExprKind::Attribute { value, attr, .. }
if (attr == "Counter"
|| attr == "OrderedDict"
|| attr == "defaultdict"
|| attr == "deque") =>
{
match &value.node {
ExprKind::Name { id, .. } if id == "collections" => {
checker.add_check(Check::new(
CheckKind::MutableArgumentDefault,
checker.locate_check(Range::from_located(expr)),
));
}
_ => {}
}
}
_ => {}
},
_ => {}
}
}
}

View File

@@ -0,0 +1,22 @@
use rustpython_ast::{Excepthandler, ExcepthandlerKind, ExprKind};
use crate::ast::types::{CheckLocator, Range};
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// B013
pub fn redundant_tuple_in_exception_handler(checker: &mut Checker, handlers: &[Excepthandler]) {
for handler in handlers {
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
if let Some(type_) = type_ {
if let ExprKind::Tuple { elts, .. } = &type_.node {
if elts.len() == 1 {
checker.add_check(Check::new(
CheckKind::RedundantTupleInExceptionHandler(elts[0].to_string()),
checker.locate_check(Range::from_located(type_)),
));
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
use rustpython_ast::{Expr, ExprKind, Unaryop};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// B002
pub fn unary_prefix_increment(checker: &mut Checker, expr: &Expr, op: &Unaryop, operand: &Expr) {
if matches!(op, Unaryop::UAdd) {
if let ExprKind::UnaryOp { op, .. } = &operand.node {
if matches!(op, Unaryop::UAdd) {
checker.add_check(Check::new(
CheckKind::UnaryPrefixIncrement,
Range::from_located(expr),
))
}
}
}
}

View File

@@ -0,0 +1,79 @@
use std::collections::BTreeMap;
use rustpython_ast::{Expr, ExprKind, Stmt};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// Identify all `ExprKind::Name` nodes in an AST.
struct NameFinder<'a> {
/// A map from identifier to defining expression.
names: BTreeMap<&'a str, &'a Expr>,
}
impl NameFinder<'_> {
fn new() -> Self {
NameFinder {
names: Default::default(),
}
}
}
impl<'a, 'b> Visitor<'b> for NameFinder<'a>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'a Expr) {
if let ExprKind::Name { id, .. } = &expr.node {
self.names.insert(id, expr);
}
visitor::walk_expr(self, expr);
}
}
/// B007
pub fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body: &[Stmt]) {
let control_names = {
let mut finder = NameFinder::new();
finder.visit_expr(target);
finder.names
};
let used_names = {
let mut finder = NameFinder::new();
for stmt in body {
finder.visit_stmt(stmt);
}
finder.names
};
for (name, expr) in control_names {
// Ignore names that are already underscore-prefixed.
if name.starts_with('_') {
continue;
}
// Ignore any names that are actually used in the loop body.
if used_names.contains_key(name) {
continue;
}
let mut check = Check::new(
CheckKind::UnusedLoopControlVariable(name.to_string()),
Range::from_located(expr),
);
if checker.patch() {
// Prefix the variable name with an underscore.
check.amend(Fix::replacement(
format!("_{name}"),
expr.location,
expr.end_location.unwrap(),
))
}
checker.add_check(check);
}
}

View File

@@ -0,0 +1,20 @@
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::flake8_builtins::types::ShadowingType;
use crate::python::builtins::BUILTINS;
/// Check builtin name shadowing.
pub fn builtin_shadowing(name: &str, location: Range, node_type: ShadowingType) -> Option<Check> {
if BUILTINS.contains(&name) {
Some(Check::new(
match node_type {
ShadowingType::Variable => CheckKind::BuiltinVariableShadowing(name.to_string()),
ShadowingType::Argument => CheckKind::BuiltinArgumentShadowing(name.to_string()),
ShadowingType::Attribute => CheckKind::BuiltinAttributeShadowing(name.to_string()),
},
location,
))
} else {
None
}
}

View File

@@ -0,0 +1,2 @@
pub mod checks;
pub mod types;

View File

@@ -0,0 +1,5 @@
pub enum ShadowingType {
Variable,
Argument,
Attribute,
}

View File

@@ -0,0 +1,420 @@
use num_bigint::BigInt;
use rustpython_ast::{
Comprehension, Constant, Expr, ExprKind, Keyword, KeywordData, Located, Unaryop,
};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
fn function_name(func: &Expr) -> Option<&str> {
if let ExprKind::Name { id, .. } = &func.node {
Some(id)
} else {
None
}
}
fn exactly_one_argument_with_matching_function<'a>(
name: &str,
func: &Expr,
args: &'a [Expr],
keywords: &[Keyword],
) -> Option<&'a ExprKind> {
if !keywords.is_empty() {
return None;
}
if args.len() != 1 {
return None;
}
if function_name(func)? != name {
return None;
}
Some(&args[0].node)
}
fn first_argument_with_matching_function<'a>(
name: &str,
func: &Expr,
args: &'a [Expr],
) -> Option<&'a ExprKind> {
if function_name(func)? != name {
return None;
}
Some(&args.first()?.node)
}
/// C400 (`list(generator)`)
pub fn unnecessary_generator_list(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("list", func, args, keywords)?;
if let ExprKind::GeneratorExp { .. } = argument {
return Some(Check::new(
CheckKind::UnnecessaryGeneratorList,
Range::from_located(expr),
));
}
None
}
/// C401 (`set(generator)`)
pub fn unnecessary_generator_set(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("set", func, args, keywords)?;
if let ExprKind::GeneratorExp { .. } = argument {
return Some(Check::new(
CheckKind::UnnecessaryGeneratorSet,
Range::from_located(expr),
));
}
None
}
/// C402 (`dict((x, y) for x, y in iterable)`)
pub fn unnecessary_generator_dict(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("dict", func, args, keywords)?;
if let ExprKind::GeneratorExp { elt, .. } = argument {
match &elt.node {
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryGeneratorDict,
Range::from_located(expr),
));
}
_ => {}
}
}
None
}
/// C403 (`set([...])`)
pub fn unnecessary_list_comprehension_set(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("set", func, args, keywords)?;
if let ExprKind::ListComp { .. } = &argument {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionSet,
Range::from_located(expr),
));
}
None
}
/// C404 (`dict([...])`)
pub fn unnecessary_list_comprehension_dict(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("dict", func, args, keywords)?;
if let ExprKind::ListComp { elt, .. } = &argument {
match &elt.node {
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionDict,
Range::from_located(expr),
));
}
_ => {}
}
}
None
}
/// C405 (`set([1, 2])`)
pub fn unnecessary_literal_set(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("set", func, args, keywords)?;
let kind = match argument {
ExprKind::List { .. } => "list",
ExprKind::Tuple { .. } => "tuple",
_ => return None,
};
Some(Check::new(
CheckKind::UnnecessaryLiteralSet(kind.to_string()),
Range::from_located(expr),
))
}
/// C406 (`dict([(1, 2)])`)
pub fn unnecessary_literal_dict(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("dict", func, args, keywords)?;
let (kind, elts) = match argument {
ExprKind::Tuple { elts, .. } => ("tuple", elts),
ExprKind::List { elts, .. } => ("list", elts),
_ => return None,
};
if let Some(elt) = elts.first() {
// dict((1, 2), ...)) or dict([(1, 2), ...])
if !matches!(&elt.node, ExprKind::Tuple { elts, .. } if elts.len() == 2) {
return None;
}
}
Some(Check::new(
CheckKind::UnnecessaryLiteralDict(kind.to_string()),
Range::from_located(expr),
))
}
/// C408
pub fn unnecessary_collection_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Located<KeywordData>],
) -> Option<Check> {
if !args.is_empty() {
return None;
}
let id = function_name(func)?;
match id {
"dict" if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) => (),
"list" | "tuple" => {
// list() or tuple()
}
_ => return None,
};
Some(Check::new(
CheckKind::UnnecessaryCollectionCall(id.to_string()),
Range::from_located(expr),
))
}
/// C409
pub fn unnecessary_literal_within_tuple_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
let argument = first_argument_with_matching_function("tuple", func, args)?;
let argument_kind = match argument {
ExprKind::Tuple { .. } => "tuple",
ExprKind::List { .. } => "list",
_ => return None,
};
Some(Check::new(
CheckKind::UnnecessaryLiteralWithinTupleCall(argument_kind.to_string()),
Range::from_located(expr),
))
}
/// C410
pub fn unnecessary_literal_within_list_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
let argument = first_argument_with_matching_function("list", func, args)?;
let argument_kind = match argument {
ExprKind::Tuple { .. } => "tuple",
ExprKind::List { .. } => "list",
_ => return None,
};
Some(Check::new(
CheckKind::UnnecessaryLiteralWithinListCall(argument_kind.to_string()),
Range::from_located(expr),
))
}
/// C411
pub fn unnecessary_list_call(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let argument = first_argument_with_matching_function("list", func, args)?;
if let ExprKind::ListComp { .. } = argument {
return Some(Check::new(
CheckKind::UnnecessaryListCall,
Range::from_located(expr),
));
}
None
}
/// C413
pub fn unnecessary_call_around_sorted(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let outer = function_name(func)?;
if !(outer == "list" || outer == "reversed") {
return None;
}
if let ExprKind::Call { func, .. } = &args.first()?.node {
if function_name(func)? == "sorted" {
return Some(Check::new(
CheckKind::UnnecessaryCallAroundSorted(outer.to_string()),
Range::from_located(expr),
));
}
}
None
}
/// C414
pub fn unnecessary_double_cast_or_process(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
let outer = function_name(func)?;
if !["list", "tuple", "set", "reversed", "sorted"].contains(&outer) {
return None;
}
fn new_check(inner: &str, outer: &str, expr: &Expr) -> Check {
Check::new(
CheckKind::UnnecessaryDoubleCastOrProcess(inner.to_string(), outer.to_string()),
Range::from_located(expr),
)
}
if let ExprKind::Call { func, .. } = &args.first()?.node {
let inner = function_name(func)?;
// Ex) set(tuple(...))
if (outer == "set" || outer == "sorted")
&& (inner == "list" || inner == "tuple" || inner == "reversed" || inner == "sorted")
{
return Some(new_check(inner, outer, expr));
}
// Ex) list(tuple(...))
if (outer == "list" || outer == "tuple") && (inner == "list" || inner == "tuple") {
return Some(new_check(inner, outer, expr));
}
// Ex) set(set(...))
if outer == "set" && inner == "set" {
return Some(new_check(inner, outer, expr));
}
}
None
}
/// C415
pub fn unnecessary_subscript_reversal(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let first_arg = args.first()?;
let id = function_name(func)?;
if !["set", "sorted", "reversed"].contains(&id) {
return None;
}
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
}
/// C416
pub fn unnecessary_comprehension(
expr: &Expr,
elt: &Expr,
generators: &[Comprehension],
) -> Option<Check> {
if generators.len() != 1 {
return None;
}
let generator = &generators[0];
if !(generator.ifs.is_empty() && generator.is_async == 0) {
return None;
}
let elt_id = function_name(elt)?;
let target_id = function_name(&generator.target)?;
if elt_id != target_id {
return None;
}
let expr_kind = match &expr.node {
ExprKind::ListComp { .. } => "list",
ExprKind::SetComp { .. } => "set",
_ => return None,
};
Some(Check::new(
CheckKind::UnnecessaryComprehension(expr_kind.to_string()),
Range::from_located(expr),
))
}
/// C417
pub fn unnecessary_map(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
fn new_check(kind: &str, expr: &Expr) -> Check {
Check::new(
CheckKind::UnnecessaryMap(kind.to_string()),
Range::from_located(expr),
)
}
let id = function_name(func)?;
match id {
"map" => {
if args.len() == 2 && matches!(&args[0].node, ExprKind::Lambda { .. }) {
return Some(new_check("generator", expr));
}
}
"list" | "set" => {
if let ExprKind::Call { func, args, .. } = &args.first()?.node {
let argument = first_argument_with_matching_function("map", func, args)?;
if let ExprKind::Lambda { .. } = argument {
return Some(new_check(id, expr));
}
}
}
"dict" => {
if args.len() == 1 {
if let ExprKind::Call { func, args, .. } = &args[0].node {
let argument = first_argument_with_matching_function("map", func, args)?;
if let ExprKind::Lambda { body, .. } = &argument {
if matches!(&body.node, ExprKind::Tuple { elts, .. } | ExprKind::List { elts, .. } if elts.len() == 2)
{
return Some(new_check(id, expr));
}
}
}
}
}
_ => (),
}
None
}

View File

@@ -0,0 +1 @@
pub mod checks;

View File

@@ -0,0 +1,36 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
/// Check whether a function call is a `print` or `pprint` invocation
pub fn print_call(
expr: &Expr,
func: &Expr,
check_print: bool,
check_pprint: bool,
) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if check_print && id == "print" {
return Some(Check::new(CheckKind::PrintFound, Range::from_located(expr)));
} else if check_pprint && id == "pprint" {
return Some(Check::new(
CheckKind::PPrintFound,
Range::from_located(expr),
));
}
}
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if let ExprKind::Name { id, .. } = &value.node {
if check_pprint && id == "pprint" && attr == "pprint" {
return Some(Check::new(
CheckKind::PPrintFound,
Range::from_located(expr),
));
}
}
}
None
}

2
src/flake8_print/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
mod checks;
pub mod plugins;

View File

@@ -0,0 +1,3 @@
pub use print_call::print_call;
mod print_call;

View File

@@ -1,19 +1,19 @@
use log::error;
use rustpython_ast::{Expr, Stmt, StmtKind};
use crate::ast::checkers;
use crate::autofix::{fixer, fixes};
use crate::autofix::helpers;
use crate::check_ast::Checker;
use crate::checks::CheckCode;
use crate::flake8_print::checks;
pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
if let Some(mut check) = checkers::print_call(
if let Some(mut check) = checks::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) {
if checker.patch() {
let context = checker.binding_context();
if matches!(
checker.parents[context.defined_by].node,
@@ -25,13 +25,13 @@ pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
.map(|index| checker.parents[*index])
.collect();
match fixes::remove_stmt(
match helpers::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" {
if fix.patch.content.is_empty() || fix.patch.content == "pass" {
checker.deletions.insert(context.defined_by);
}
check.amend(fix)

298
src/flake8_quotes/checks.rs Normal file
View File

@@ -0,0 +1,298 @@
use rustpython_ast::Location;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::flake8_quotes::settings::{Quote, Settings};
use crate::source_code_locator::SourceCodeLocator;
fn good_single(quote: &Quote) -> char {
match quote {
Quote::Single => '\'',
Quote::Double => '"',
}
}
fn bad_single(quote: &Quote) -> char {
match quote {
Quote::Double => '\'',
Quote::Single => '"',
}
}
fn good_multiline(quote: &Quote) -> &str {
match quote {
Quote::Single => "'''",
Quote::Double => "\"\"\"",
}
}
fn good_multiline_ending(quote: &Quote) -> &str {
match quote {
Quote::Single => "'\"\"\"",
Quote::Double => "\"'''",
}
}
fn good_docstring(quote: &Quote) -> &str {
match quote {
Quote::Single => "'''",
Quote::Double => "\"\"\"",
}
}
pub fn quotes(
locator: &SourceCodeLocator,
start: &Location,
end: &Location,
is_docstring: bool,
settings: &Settings,
) -> Option<Check> {
let text = locator.slice_source_code_range(&Range {
location: *start,
end_location: *end,
});
// Remove any prefixes (e.g., remove `u` from `u"foo"`).
let last_quote_char = text.chars().last().unwrap();
let first_quote_char = text.find(last_quote_char).unwrap();
let prefix = &text[..first_quote_char].to_lowercase();
let raw_text = &text[first_quote_char..];
// Determine if the string is multiline-based.
let is_multiline = if raw_text.len() >= 3 {
let mut chars = raw_text.chars();
let first = chars.next().unwrap();
let second = chars.next().unwrap();
let third = chars.next().unwrap();
first == second && second == third
} else {
false
};
if is_docstring {
if raw_text.contains(good_docstring(&settings.docstring_quotes)) {
return None;
}
return Some(Check::new(
CheckKind::BadQuotesDocstring(settings.docstring_quotes.clone()),
Range {
location: *start,
end_location: *end,
},
));
} else if is_multiline {
// If our string is or contains a known good string, ignore it.
if raw_text.contains(good_multiline(&settings.multiline_quotes)) {
return None;
}
// If our string ends with a known good ending, then ignore it.
if raw_text.ends_with(good_multiline_ending(&settings.multiline_quotes)) {
return None;
}
return Some(Check::new(
CheckKind::BadQuotesMultilineString(settings.multiline_quotes.clone()),
Range {
location: *start,
end_location: *end,
},
));
} else {
let string_contents = &raw_text[1..raw_text.len() - 1];
// If we're using the preferred quotation type, check for escapes.
if last_quote_char == good_single(&settings.inline_quotes) {
if !settings.avoid_escape || prefix.contains('r') {
return None;
}
if string_contents.contains(good_single(&settings.inline_quotes))
&& !string_contents.contains(bad_single(&settings.inline_quotes))
{
return Some(Check::new(
CheckKind::AvoidQuoteEscape,
Range {
location: *start,
end_location: *end,
},
));
}
return None;
}
// If we're not using the preferred type, only allow use to avoid escapes.
if !string_contents.contains(good_single(&settings.inline_quotes)) {
return Some(Check::new(
CheckKind::BadQuotesInlineString(settings.inline_quotes.clone()),
Range {
location: *start,
end_location: *end,
},
));
}
}
None
}
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use rustpython_parser::lexer::LexResult;
use test_case::test_case;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::flake8_quotes::settings::Quote;
use crate::linter::tokenize;
use crate::{flake8_quotes, linter, Settings};
use crate::{fs, noqa};
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
linter::check_path(path, &contents, tokens, &noqa_line_for, settings, autofix)
}
#[test_case(Path::new("doubles.py"))]
#[test_case(Path::new("doubles_escaped.py"))]
#[test_case(Path::new("doubles_multiline_string.py"))]
#[test_case(Path::new("doubles_noqa.py"))]
#[test_case(Path::new("doubles_wrapped.py"))]
fn doubles(path: &Path) -> Result<()> {
let snapshot = format!("doubles_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("singles.py"))]
#[test_case(Path::new("singles_escaped.py"))]
#[test_case(Path::new("singles_multiline_string.py"))]
#[test_case(Path::new("singles_noqa.py"))]
#[test_case(Path::new("singles_wrapped.py"))]
fn singles(path: &Path) -> Result<()> {
let snapshot = format!("singles_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Double,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("docstring_doubles.py"))]
#[test_case(Path::new("docstring_doubles_module_multiline.py"))]
#[test_case(Path::new("docstring_doubles_module_singleline.py"))]
#[test_case(Path::new("docstring_doubles_class.py"))]
#[test_case(Path::new("docstring_doubles_function.py"))]
#[test_case(Path::new("docstring_singles.py"))]
#[test_case(Path::new("docstring_singles_module_multiline.py"))]
#[test_case(Path::new("docstring_singles_module_singleline.py"))]
#[test_case(Path::new("docstring_singles_class.py"))]
#[test_case(Path::new("docstring_singles_function.py"))]
fn double_docstring(path: &Path) -> Result<()> {
let snapshot = format!("double_docstring_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("docstring_doubles.py"))]
#[test_case(Path::new("docstring_doubles_module_multiline.py"))]
#[test_case(Path::new("docstring_doubles_module_singleline.py"))]
#[test_case(Path::new("docstring_doubles_class.py"))]
#[test_case(Path::new("docstring_doubles_function.py"))]
#[test_case(Path::new("docstring_singles.py"))]
#[test_case(Path::new("docstring_singles_module_multiline.py"))]
#[test_case(Path::new("docstring_singles_module_singleline.py"))]
#[test_case(Path::new("docstring_singles_class.py"))]
#[test_case(Path::new("docstring_singles_function.py"))]
fn single_docstring(path: &Path) -> Result<()> {
let snapshot = format!("single_docstring_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
}

View File

@@ -0,0 +1,118 @@
//! Extract docstrings via tokenization.
//!
//! See: https://github.com/zheller/flake8-quotes/blob/ef0d9a90249a080e460b70ab62bf4b65e5aa5816/flake8_quotes/docstring_detection.py#L29
//!
//! TODO(charlie): Consolidate with the existing AST-based docstring extraction.
use rustpython_parser::lexer::Tok;
#[derive(Debug)]
enum State {
// Start of the module: first string gets marked as a docstring.
ExpectModuleDocstring,
// After seeing a class definition, we're waiting for the block colon (and do bracket counting).
ExpectClassColon,
// After seeing the block colon in a class definition, we expect a docstring.
ExpectClassDocstring,
// Same as ExpectClassColon, but for function definitions.
ExpectFunctionColon,
// Same as ExpectClassDocstring, but for function definitions.
ExpectFunctionDocstring,
// Skip tokens until we observe a `class` or `def`.
Other,
}
pub struct StateMachine {
state: State,
bracket_count: usize,
}
impl StateMachine {
pub fn new() -> Self {
Self {
state: State::ExpectModuleDocstring,
bracket_count: 0,
}
}
pub fn consume(&mut self, tok: &Tok) -> bool {
if matches!(tok, Tok::Newline | Tok::Indent | Tok::Dedent) {
return false;
}
if matches!(tok, Tok::String { .. }) {
return if matches!(
self.state,
State::ExpectModuleDocstring
| State::ExpectClassDocstring
| State::ExpectFunctionDocstring
) {
self.state = State::Other;
true
} else {
false
};
}
if matches!(tok, Tok::Class) {
self.state = State::ExpectClassColon;
self.bracket_count = 0;
return false;
}
if matches!(tok, Tok::Def) {
self.state = State::ExpectFunctionColon;
self.bracket_count = 0;
return false;
}
if matches!(tok, Tok::Colon) {
if self.bracket_count == 0 {
if matches!(self.state, State::ExpectClassColon) {
self.state = State::ExpectClassDocstring;
} else if matches!(self.state, State::ExpectFunctionColon) {
self.state = State::ExpectFunctionDocstring;
}
}
return false;
}
if matches!(tok, Tok::Lpar | Tok::Lbrace | Tok::Lsqb) {
self.bracket_count += 1;
if matches!(
self.state,
State::ExpectModuleDocstring
| State::ExpectClassDocstring
| State::ExpectFunctionDocstring
) {
self.state = State::Other;
}
return false;
}
if matches!(tok, Tok::Rpar | Tok::Rbrace | Tok::Rsqb) {
self.bracket_count -= 1;
if matches!(
self.state,
State::ExpectModuleDocstring
| State::ExpectClassDocstring
| State::ExpectFunctionDocstring
) {
self.state = State::Other;
}
return false;
}
if matches!(
self.state,
State::ExpectModuleDocstring
| State::ExpectClassDocstring
| State::ExpectFunctionDocstring
) {
self.state = State::Other;
return false;
}
false
}
}

3
src/flake8_quotes/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod checks;
pub mod docstring_detection;
pub mod settings;

View File

@@ -0,0 +1,49 @@
//! Settings for the `flake-quotes` plugin.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub enum Quote {
Single,
Double,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
pub inline_quotes: Option<Quote>,
pub multiline_quotes: Option<Quote>,
pub docstring_quotes: Option<Quote>,
pub avoid_escape: Option<bool>,
}
#[derive(Debug)]
pub struct Settings {
pub inline_quotes: Quote,
pub multiline_quotes: Quote,
pub docstring_quotes: Quote,
pub avoid_escape: bool,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
Self {
inline_quotes: options.inline_quotes.unwrap_or(Quote::Single),
multiline_quotes: options.multiline_quotes.unwrap_or(Quote::Double),
docstring_quotes: options.docstring_quotes.unwrap_or(Quote::Double),
avoid_escape: options.avoid_escape.unwrap_or(true),
}
}
}
impl Default for Settings {
fn default() -> Self {
Self {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Double,
avoid_escape: true,
}
}
}

View File

@@ -0,0 +1,50 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 5
column: 0
end_location:
row: 7
column: 3
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 16
column: 4
end_location:
row: 18
column: 7
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 21
column: 20
end_location:
row: 22
column: 37
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 30
column: 8
end_location:
row: 32
column: 11
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 35
column: 12
end_location:
row: 37
column: 15
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 3
column: 4
end_location:
row: 3
column: 27
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 5
column: 22
end_location:
row: 5
column: 43
fix: ~

View File

@@ -0,0 +1,50 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 3
column: 4
end_location:
row: 3
column: 26
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 11
column: 4
end_location:
row: 11
column: 26
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 15
column: 38
end_location:
row: 17
column: 3
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 17
column: 4
end_location:
row: 17
column: 19
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 21
column: 4
end_location:
row: 21
column: 27
fix: ~

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