Compare commits

..

135 Commits

Author SHA1 Message Date
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
Charlie Marsh
6ee3075867 Bump version to 0.0.78 2022-10-16 21:28:25 -04:00
Charlie Marsh
cc8a945cbf Tweak messages for flake8-comprehensions rules (#444) 2022-10-16 21:27:13 -04:00
Charlie Marsh
1a1922b3fc Re-add the fix icon to README.md (#443) 2022-10-16 21:21:30 -04:00
Charlie Marsh
48bd766298 Implement autofixes for more docstring rules (#442) 2022-10-16 21:16:57 -04:00
Charlie Marsh
1ece3873cd Implement autofix for newline-related docstring rules (#441) 2022-10-16 19:40:38 -04:00
Charlie Marsh
472d902486 Rename docstring_plugins.rs to plugins.rs 2022-10-16 18:39:08 -04:00
Charlie Marsh
4ac6a18d40 Remove some Vec arguments 2022-10-16 18:17:10 -04:00
Charlie Marsh
8a47ea91ba Use CheckCategory to drive default rules 2022-10-16 18:07:03 -04:00
Charlie Marsh
bd4394aa89 Capitalize pep8-naming messages 2022-10-16 17:58:26 -04:00
Charlie Marsh
56f69ce71e Bump version to 0.0.77 2022-10-16 13:43:52 -04:00
Charlie Marsh
248a6cd50b Remove offsets hacks for docstring parsing logic (#440) 2022-10-16 13:43:30 -04:00
Charlie Marsh
d9e659d817 Revert "Remove trailing colon from messages"
This reverts commit 77e5564f4b.
2022-10-16 12:35:40 -04:00
Charlie Marsh
77e5564f4b Remove trailing colon from messages 2022-10-16 12:09:46 -04:00
Charlie Marsh
e79766d5ec Use backticks for pep8-naming messages 2022-10-16 12:09:07 -04:00
Harutaka Kawamura
c55fd76743 Implement N801 ~ N805 (#439) 2022-10-16 11:58:39 -04:00
Charlie Marsh
e2aedc5ba8 Bump version to 0.0.76 2022-10-15 17:22:12 -04:00
Charlie Marsh
1b2d085460 Mark W292 as a non-AST check 2022-10-15 17:21:52 -04:00
Charlie Marsh
cb138526b1 Add a note on pydocstyle 2022-10-15 17:05:18 -04:00
Charlie Marsh
8eac270d8f Add an FAQ 2022-10-15 16:52:20 -04:00
Charlie Marsh
6e19fd20bb Add table of contents 2022-10-15 16:37:52 -04:00
Charlie Marsh
10868445f5 Use H3 for rules sections 2022-10-15 16:33:26 -04:00
Charlie Marsh
e3ecf21287 Break rules table into sections (#437) 2022-10-15 16:29:08 -04:00
Charlie Marsh
fd849e112e Remove checkmark from rule table (#436) 2022-10-15 11:28:32 -04:00
Harutaka Kawamura
af27471c77 Fix C401 and C402 (#435) 2022-10-15 09:16:43 -04:00
konstin
bb466bc8d3 Add initial wasm32-wasi support (#416) 2022-10-14 20:58:18 -04:00
Charlie Marsh
7741a713e2 Avoid checking for updates when executing via stdin (#433) 2022-10-14 20:57:40 -04:00
Charlie Marsh
3ab1cfc6f8 Make some improvements to the README 2022-10-14 17:30:40 -04:00
Charlie Marsh
e73f13473d Add a .flake8 file for benchmarking 2022-10-14 17:05:24 -04:00
Charlie Marsh
3e8ef5b40f Bump version to 0.0.75 2022-10-14 14:42:57 -04:00
Charlie Marsh
c59610906c Optimize imports 2022-10-14 14:42:48 -04:00
Charlie Marsh
2353a52be8 Nest if 2022-10-14 14:36:45 -04:00
Charlie Marsh
bbffdd57ff Handle multi-byte chars in SourceCodeLocator (#431) 2022-10-14 14:29:18 -04:00
Charlie Marsh
3c15c578a7 Implement D206, D207, and D208 (#429) 2022-10-14 13:26:36 -04:00
Charlie Marsh
6a8e31b2ff Bump version to 0.0.74 2022-10-14 12:36:44 -04:00
Charlie Marsh
6407fd5a33 Re-arrange some docstring modules (#428) 2022-10-14 12:34:35 -04:00
Harutaka Kawamura
b64040cbb2 Implement C417 (#426) 2022-10-14 12:34:00 -04:00
Charlie Marsh
952a0eb4e3 Implement checks for Google-style docstrings (#427) 2022-10-14 11:53:29 -04:00
Charlie Marsh
3e28d6de04 Bump version to 0.0.73 2022-10-14 10:18:42 -04:00
Charlie Marsh
9bbfd1d3b2 Implement docstring argument tracking for NumPy-style docstrings (#425) 2022-10-14 10:18:07 -04:00
Charlie Marsh
6fb82ab763 Use test_case for macro-driven check tests (#424) 2022-10-13 18:51:01 -04:00
Charlie Marsh
6b286e9bc1 Add --config as a command-line option (#422) 2022-10-13 18:13:41 -04:00
Harutaka Kawamura
bcddd9e97f Implement C413 (#421) 2022-10-13 11:15:41 -04:00
Harutaka Kawamura
3e789136af Implement C411 (#420) 2022-10-13 09:48:27 -04:00
Harutaka Kawamura
07ef3b8754 Implement C416 (#415) 2022-10-13 08:34:33 -04:00
Charlie Marsh
46e1b16472 Bump version to 0.0.72 2022-10-12 22:43:29 -04:00
fsouza
720bfe0161 Implement --fix with stdin (#405) 2022-10-12 22:31:46 -04:00
Charlie Marsh
2f69be0d41 Bump version to 0.0.71 2022-10-12 17:14:28 -04:00
Charlie Marsh
54cb2eb15b Only run section checks when CheckCodes are enabled 2022-10-12 17:14:16 -04:00
Charlie Marsh
167992ad48 Implement D407, D408, D409, D412, and D414 (#413) 2022-10-12 17:12:54 -04:00
Charlie Marsh
f0dab24079 Implement D405, D406, D410, D411, and D413 (#411) 2022-10-12 16:31:14 -04:00
Charlie Marsh
77055faab6 Implement D404 and D418 for pydocstyle (#409) 2022-10-12 13:20:55 -04:00
Charlie Marsh
e08e1caf71 Bump version to 0.0.70 2022-10-12 12:59:14 -04:00
Harutaka Kawamura
0072dfd81e Implement C414 (#406) 2022-10-12 12:58:46 -04:00
Charlie Marsh
6ffe02ee05 Pass around VisibleScope 2022-10-12 12:52:48 -04:00
Charlie Marsh
688fc0cd02 Implement docstring visibility checks (#408) 2022-10-12 12:46:40 -04:00
Charlie Marsh
f30e5e45ab Remove initial field 2022-10-12 11:07:31 -04:00
Charlie Marsh
1a68a38306 Enable definition tracking for docstrings (#407) 2022-10-12 11:06:28 -04:00
Charlie Marsh
590aa92ead Implement D201, D202, D203, D204, and D211 (#404) 2022-10-11 21:08:30 -04:00
Charlie Marsh
8868f57a74 Implement D402 for pydocstyle (#403) 2022-10-11 13:19:56 -04:00
Charlie Marsh
71802f8861 Bump version to 0.0.69 2022-10-11 12:54:56 -04:00
Charlie Marsh
5b6fb8cefa Skip docstring checks for empty docstrings (#402) 2022-10-11 12:54:30 -04:00
Charlie Marsh
2ff964107c Add D212, D213, D300, D403, and D415 (#400) 2022-10-11 12:48:23 -04:00
Charlie Marsh
141132d5be Add fake setup.py (#399) 2022-10-11 12:38:48 -04:00
Harutaka Kawamura
8ba872ece4 Support linting input from stdin (#387) 2022-10-11 09:56:20 -04:00
Charlie Marsh
209dce2033 Fix missing backtick 2022-10-10 17:23:58 -04:00
Charlie Marsh
90d88dfb10 Make pyupgrade tally more precise 2022-10-10 17:18:20 -04:00
Charlie Marsh
4730911b25 Bump version to 0.0.68 2022-10-10 16:50:09 -04:00
Charlie Marsh
4e9fb9907a Implement D205, D209, and D210 (#398) 2022-10-10 16:49:51 -04:00
Charlie Marsh
b8dce8922d Implement D200 (OneLinerDocstring) (#397) 2022-10-10 16:07:51 -04:00
Charlie Marsh
30877127bc Implement D400 (DocstringEndsInNonPeriod) (#396) 2022-10-10 15:35:23 -04:00
Charlie Marsh
8b66bbdc9b Regenerate rules table 2022-10-10 15:18:18 -04:00
Charlie Marsh
71d3a84b14 Implement D410 (EmptyDocstring) (#395) 2022-10-10 15:15:38 -04:00
Charlie Marsh
323a5c857c Implement docstring tracking (#394) 2022-10-10 15:15:09 -04:00
Charlie Marsh
42cec3f5a0 Regenerate rules table 2022-10-10 14:02:31 -04:00
Charlie Marsh
ee42413e10 Enable autofix for B014 2022-10-10 13:18:43 -04:00
Charlie Marsh
765db12b84 Remove check_ prefix from check utilities (#393) 2022-10-10 12:58:40 -04:00
Charlie Marsh
e1b711d9c6 Bump version to 0.0.67 2022-10-10 12:55:04 -04:00
Charlie Marsh
35f593846e Implement B014 from flake8-bugbear (#392) 2022-10-10 12:53:42 -04:00
Charlie Marsh
c384fa513b Implement B025 from flake8-bugbear (#391) 2022-10-10 12:18:31 -04:00
Charlie Marsh
022ff64d29 Implement B011 from flake8-bugbear (#390) 2022-10-10 10:55:55 -04:00
Charlie Marsh
5a06fb28fd Bump version to 0.0.66 2022-10-10 10:03:59 -04:00
Charlie Marsh
46750a3e17 Flag unimplemented error codes in M001 (#388) 2022-10-10 10:03:40 -04:00
Charlie Marsh
9cc902b802 Avoid F821 false-positives with NameError (#386) 2022-10-10 09:39:59 -04:00
Harutaka Kawamura
c2a36ebd1e Implement C410 (#382) 2022-10-09 23:33:55 -04:00
Charlie Marsh
34ca225393 Rename flakes8 to flake8 2022-10-09 23:02:44 -04:00
Harutaka Kawamura
38c30905e6 Implement C409 (#381) 2022-10-09 22:34:52 -04:00
Charlie Marsh
2774194b03 Bump version to 0.0.65 2022-10-09 22:14:04 -04:00
Charlie Marsh
71ebd39f35 Extend assertEquals check to all deprecated unittest aliases (#380) 2022-10-09 22:13:51 -04:00
Charlie Marsh
a2f78ba2c7 Fix auto-fix for assertEquals rename 2022-10-09 22:11:32 -04:00
Charlie Marsh
b51a080a44 Rename SPR001 to U008 (#379) 2022-10-09 21:58:22 -04:00
Charlie Marsh
6a1d7d8a1c Defer string annotations even when futures annotations are enabled (#378) 2022-10-09 18:28:29 -04:00
279 changed files with 13301 additions and 3875 deletions

280
Cargo.lock generated
View File

@@ -64,6 +64,20 @@ dependencies = [
"term",
]
[[package]]
name = "assert_cmd"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e"
dependencies = [
"bstr",
"doc-comment",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "async-channel"
version = "1.7.1"
@@ -404,9 +418,9 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "clap"
version = "4.0.9"
version = "4.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30607dd93c420c6f1f80b544be522a0238a7db35e6a12968d28910983fee0df0"
checksum = "6bf8832993da70a4c6d13c581f4463c2bdda27b9bf1c5498dc4365543abe6d6f"
dependencies = [
"atty",
"bitflags",
@@ -419,9 +433,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.0.9"
version = "4.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a307492e1a34939f79d3b6b9650bd2b971513cd775436bf2b78defeb5af00b"
checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad"
dependencies = [
"heck",
"proc-macro-error",
@@ -452,6 +466,16 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "colored"
version = "2.0.0"
@@ -574,12 +598,62 @@ dependencies = [
"syn",
]
[[package]]
name = "cxx"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199"
dependencies = [
"cc",
"codespan-reporting",
"once_cell",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c"
[[package]]
name = "cxxbridge-macro"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.8.1"
@@ -658,6 +732,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "either"
version = "1.8.0"
@@ -928,8 +1008,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -955,6 +1037,9 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "heck"
@@ -991,17 +1076,28 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "iana-time-zone"
version = "0.1.50"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0"
checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"winapi 0.3.9",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa"
dependencies = [
"cxx",
"cxx-build",
]
[[package]]
name = "idna"
version = "0.3.0"
@@ -1085,9 +1181,15 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
[[package]]
name = "joinery"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5"
[[package]]
name = "js-sys"
@@ -1193,9 +1295,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.134"
version = "0.2.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
[[package]]
name = "libcst"
@@ -1221,6 +1323,15 @@ dependencies = [
"syn",
]
[[package]]
name = "link-cplusplus"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
dependencies = [
"cc",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@@ -1506,18 +1617,18 @@ checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
[[package]]
name = "path-absolutize"
version = "3.0.13"
version = "3.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3de4b40bd9736640f14c438304c09538159802388febb02c8abaae0846c1f13"
checksum = "0f1d4993b16f7325d90c18c3c6a3327db7808752db8d208cea0acee0abd52c52"
dependencies = [
"path-dedot",
]
[[package]]
name = "path-dedot"
version = "3.0.17"
version = "3.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d611d5291372b3738a34ebf0d1f849e58b1dcc1101032f76a346eaa1f8ddbb5b"
checksum = "9a81540d94551664b72b72829b12bd167c73c9d25fbac0e04fafa8023f7e4901"
dependencies = [
"once_cell",
]
@@ -1685,6 +1796,33 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c"
dependencies = [
"difflib",
"itertools",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb"
[[package]]
name = "predicates-tree"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -1907,9 +2045,10 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.64"
version = "0.0.86"
dependencies = [
"anyhow",
"assert_cmd",
"bincode",
"cacache",
"chrono",
@@ -1920,6 +2059,7 @@ dependencies = [
"dirs 4.0.0",
"fern",
"filetime",
"getrandom 0.2.7",
"glob",
"insta",
"itertools",
@@ -1938,6 +2078,9 @@ dependencies = [
"serde_json",
"strum",
"strum_macros",
"test-case",
"textwrap",
"titlecase",
"toml",
"update-informer",
"walkdir",
@@ -1958,7 +2101,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -1968,7 +2111,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
dependencies = [
"ascii",
"cfg-if 1.0.0",
@@ -1991,7 +2134,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
dependencies = [
"bincode",
"bitflags",
@@ -2008,7 +2151,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
dependencies = [
"ahash",
"anyhow",
@@ -2056,6 +2199,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "scratch"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]]
name = "sct"
version = "0.7.0"
@@ -2094,9 +2243,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.85"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
dependencies = [
"itoa",
"ryu",
@@ -2199,6 +2348,12 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "socket2"
version = "0.4.7"
@@ -2279,9 +2434,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.101"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
dependencies = [
"proc-macro2",
"quote",
@@ -2345,6 +2500,45 @@ dependencies = [
"phf_codegen 0.8.0",
]
[[package]]
name = "termtree"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
[[package]]
name = "test-case"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21d6cf5a7dffb3f9dceec8e6b8ca528d9bd71d36c9f074defb548ce161f598c0"
dependencies = [
"test-case-macros",
]
[[package]]
name = "test-case-macros"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e45b7bf6e19353ddd832745c8fcf77a17a93171df7151187f26623f2b75b5b26"
dependencies = [
"cfg-if 1.0.0",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "textwrap"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.37"
@@ -2400,6 +2594,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "titlecase"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38397a8cdb017cfeb48bf6c154d6de975ac69ffeed35980fde199d2ee0842042"
dependencies = [
"joinery",
"lazy_static",
"regex",
]
[[package]]
name = "toml"
version = "0.5.9"
@@ -2497,9 +2702,19 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "unicode-linebreak"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown",
"regex",
]
[[package]]
name = "unicode-normalization"
@@ -2510,6 +2725,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode-xid"
version = "0.2.4"
@@ -2593,6 +2814,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8e76fae08f03f96e166d2dfda232190638c10e0383841252416f9cfe2ae60e6"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.64"
version = "0.0.86"
edition = "2021"
[lib]
@@ -9,10 +9,8 @@ name = "ruff"
[dependencies]
anyhow = { version = "1.0.60" }
bincode = { version = "1.3.3" }
cacache = { version = "10.0.1" }
chrono = { version = "0.4.21" }
clap = { version = "4.0.1", features = ["derive"] }
clearscreen = { version = "1.0.10" }
colored = { version = "2.0.0" }
common-path = { version = "1.0.0" }
dirs = { version = "4.0.0" }
@@ -23,24 +21,37 @@ itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "32a044c127668df44582f85699358e67803b0d73" }
log = { version = "0.4.17" }
notify = { version = "4.0.17" }
num-bigint = { version = "0.4.3" }
once_cell = { version = "1.13.1" }
path-absolutize = { version = "3.0.13", features = ["once_cell_cache"] }
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 = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/charliermarsh/RustPython.git", rev = "1b253a12705f84972cd76e8dc1cdaaccb233e5a5" }
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "1b253a12705f84972cd76e8dc1cdaaccb233e5a5" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "1b253a12705f84972cd76e8dc1cdaaccb233e5a5" }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
textwrap = { version = "0.15.1" }
titlecase = { version = "2.2.1" }
toml = { version = "0.5.9" }
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true }
walkdir = { version = "2.3.2" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = "0.24.3"
num-bigint = "0.4.3"
[target.'cfg(not(target_family = "wasm"))'.dependencies]
cacache = { version = "10.0.1" } # uses async-std
clearscreen = { version = "1.0.10" } # uses which
# https://docs.rs/getrandom/0.2.7/getrandom/#webassembly-support
# For (future) wasm-pack support
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
getrandom = { version = "0.2.7", features = ["js"] }
[dev-dependencies]
assert_cmd = { version = "2.0.4" }
insta = { version = "1.19.1", features = ["yaml"] }
test-case = { version = "2.2.2" }
[features]
default = ["update-informer"]

545
README.md
View File

@@ -1,4 +1,4 @@
# ruff
# Ruff
[![image](https://img.shields.io/pypi/v/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff)
@@ -20,17 +20,37 @@ An extremely fast Python linter, written in Rust.
- 🤝 Python 3.10 compatibility
- 🛠️ `pyproject.toml` support
- 📦 [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 `--fix` support
- 👀 [TypeScript](https://www.typescriptlang.org/docs/handbook/configuring-watch.html)-inspired `--watch` support
- ⚖️ [Near-complete parity](#Parity-with-Flake8) with the built-in Flake8 rule set
- 🔧 [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](#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
functionality behind a single, common interface. Ruff can be used to replace Flake8 (plus a variety
of plugins), [`pydocstyle`](https://pypi.org/project/pydocstyle/), [`yesqa`](https://github.com/asottile/yesqa),
and even a subset of [`pyupgrade`](https://pypi.org/project/pyupgrade/) and [`autoflake`](https://pypi.org/project/autoflake/)
all while executing tens or hundreds of times faster than any individual tool.
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
## Installation and usage
## Table of Contents
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)
## Installation and Usage
### Installation
Available as [ruff](https://pypi.org/project/ruff/) on PyPI:
Available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
```shell
pip install ruff
@@ -38,7 +58,7 @@ pip install ruff
### Usage
To run ruff, try any of the following:
To run Ruff, try any of the following:
```shell
ruff path/to/code/to/check.py
@@ -46,27 +66,27 @@ ruff path/to/code/
ruff path/to/code/*.py
```
You can run ruff in `--watch` mode to automatically re-run on-change:
You can run Ruff in `--watch` mode to automatically re-run on-change:
```shell
ruff path/to/code/ --watch
```
ruff also works with [pre-commit](https://pre-commit.com):
Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.48
rev: v0.0.86
hooks:
- id: lint
```
## Configuration
ruff is configurable both via `pyproject.toml` and the command line.
Ruff is configurable both via `pyproject.toml` and the command line.
For example, you could configure ruff to only enforce a subset of rules with:
For example, you could configure Ruff to only enforce a subset of rules with:
```toml
[tool.ruff]
@@ -75,6 +95,10 @@ select = [
"F401",
"F403",
]
per-file-ignores = [
"__init__.py:F401",
"path/to/file.py:F401"
]
```
Alternatively, on the command-line:
@@ -94,6 +118,8 @@ Arguments:
<FILES>...
Options:
--config <CONFIG>
Path to the `pyproject.toml` file to use for configuration
-v, --verbose
Enable verbose logging
-q, --quiet
@@ -132,6 +158,8 @@ Options:
Regular expression matching the name of dummy variables
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
-h, --help
Print help information
-V, --version
@@ -154,7 +182,7 @@ Exclusions are based on globs, and can be either:
To omit a lint check entirely, add it to the "ignore" list via `--ignore` or `--extend-ignore`,
either on the command-line or in your `project.toml` file.
To ignore an error in-line, ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
To ignore an error in-line, Ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
To ignore an individual error, add `# noqa: {code}` to the end of the line, like so:
```python
@@ -178,144 +206,235 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
""" # noqa: E501
```
ruff supports several (experimental) workflows to aid in `noqa` management.
Ruff supports several workflows to aid in `noqa` management.
First, ruff provides a special error code, `M001`, to enforce that your `noqa` directives are
First, Ruff provides a special error code, `M001`, to enforce that your `noqa` directives are
"valid", in that the errors they _say_ they ignore are actually being triggered on that line (and
thus suppressed). **You can run `ruff /path/to/file.py --extend-select M001` to flag unused `noqa`
directives.**
Second, ruff can _automatically remove_ unused `noqa` directives via its autofix functionality.
Second, Ruff can _automatically remove_ unused `noqa` directives via its autofix functionality.
**You can run `ruff /path/to/file.py --extend-select M001 --fix` to automatically remove unused
`noqa` directives.**
Third, ruff can _automatically add_ `noqa` directives to all failing lines. This is useful when
migrating a new codebase to ruff. **You can run `ruff /path/to/file.py --add-noqa` to automatically
Third, Ruff can _automatically add_ `noqa` directives to all failing lines. This is useful when
migrating a new codebase to Ruff. **You can run `ruff /path/to/file.py --add-noqa` to automatically
add `noqa` directives to all failing lines, with the appropriate error codes.**
### Compatibility with Black
## Supported Rules
ruff is compatible with [Black](https://github.com/psf/black) out-of-the-box, as long as
the `line-length` setting is consistent between the two.
As a project, ruff is designed to be used alongside Black and, as such, will defer implementing
stylistic lint rules that are obviated by autoformatting.
### Parity with Flake8
ruff's goal is to achieve feature parity with Flake8 when used (1) without plugins, (2) alongside
Black, and (3) on Python 3 code.
**Under those conditions, ruff implements 44 out of 60 rules.** (ruff is missing: 14 rules related
to string `.format` calls, 1 rule related to docstring parsing, and 1 rule related to redefined
variables.)
ruff also implements some of the most popular Flake8 plugins natively, including:
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (partial)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (partial)
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
1. ruff does not yet support a few Python 3.9 and 3.10 language features, including structural
pattern matching and parenthesized context managers.
2. Flake8 has a plugin architecture and supports writing custom lint rules.
## Rules
The ✅ emoji indicates a rule is enabled by default.
By default, Ruff enables all `E`, `W`, 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.
| Code | Name | Message | | |
| ---- | ---- | ------- | --- | --- |
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | ✅ | |
| E501 | LineTooLong | Line too long (89 > 88 characters) | ✅ | |
| E711 | NoneComparison | Comparison to `None` should be `cond is None` | ✅ | |
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | ✅ | |
| E713 | NotInTest | Test for membership should be `not in` | ✅ | |
| E714 | NotIsTest | Test for object identity should be `is not` | ✅ | |
| E721 | TypeComparison | Do not compare types, use `isinstance()` | ✅ | |
| E722 | DoNotUseBareExcept | Do not use bare `except` | ✅ | |
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | ✅ | |
| E741 | AmbiguousVariableName | Ambiguous variable name: `...` | ✅ | |
| E742 | AmbiguousClassName | Ambiguous class name: `...` | ✅ | |
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | ✅ | |
| E902 | IOError | IOError: `...` | ✅ | |
| E999 | SyntaxError | SyntaxError: `...` | ✅ | |
| W292 | NoNewLineAtEndOfFile | No newline at end of file | ✅ | |
| F401 | UnusedImport | `...` imported but unused | ✅ | 🛠 |
| F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable | ✅ | |
| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names | ✅ | |
| F404 | LateFutureImport | `from __future__` imports must occur at the beginning of the file | ✅ | |
| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | ✅ | |
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | ✅ | |
| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | ✅ | |
| F541 | FStringMissingPlaceholders | f-string without any placeholders | ✅ | |
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | ✅ | |
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | ✅ | |
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | ✅ | |
| F622 | TwoStarredExpressions | Two starred expressions in assignment | ✅ | |
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | ✅ | |
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | ✅ | |
| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | ✅ | |
| 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 | ✅ | |
| 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: `...` | ✅ | |
| F821 | UndefinedName | Undefined name `...` | ✅ | |
| F822 | UndefinedExport | Undefined name `...` in `__all__` | ✅ | |
| F823 | UndefinedLocal | Local variable `...` referenced before assignment | ✅ | |
| F831 | DuplicateArgumentName | Duplicate argument name in function definition | ✅ | |
| F841 | UnusedVariable | Local variable `...` is assigned to but never used | ✅ | |
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | ✅ | |
| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin | | |
| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin | | |
| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin | | |
| C400 | UnnecessaryGeneratorList | Unnecessary generator - rewrite as a list comprehension | | |
| C401 | UnnecessaryGeneratorSet | Unnecessary generator - rewrite as a set comprehension | | |
| C402 | UnnecessaryGeneratorDict | Unnecessary generator - rewrite as a dict comprehension | | |
| C403 | UnnecessaryListComprehensionSet | Unnecessary list comprehension - rewrite as a set comprehension | | |
| C404 | UnnecessaryListComprehensionDict | Unnecessary list comprehension - rewrite as a dict comprehension | | |
| C405 | UnnecessaryLiteralSet | Unnecessary <list/tuple> literal - rewrite as a set literal | | |
| C406 | UnnecessaryLiteralDict | Unnecessary <list/tuple> literal - rewrite as a dict literal | | |
| C408 | UnnecessaryCollectionCall | Unnecessary <dict/list/tuple> call - rewrite as a literal | | |
| C415 | UnnecessarySubscriptReversal | Unnecessary subscript reversal of iterable within <reversed/set/sorted>() | | |
| SPR001 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | | 🛠 |
| T201 | PrintFound | `print` found | | 🛠 |
| T203 | PPrintFound | `pprint` found | | 🛠 |
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | | 🛠 |
| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | | 🛠 |
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | | 🛠 |
| U004 | UselessObjectInheritance | Class `...` inherits from object | | 🛠 |
| U005 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 |
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | | 🛠 |
| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | | 🛠 |
| M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 |
### Pyflakes
## Integrations
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| F401 | UnusedImport | `...` imported but unused | 🛠 |
| F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable | |
| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names | |
| F404 | LateFutureImport | `from __future__` imports must occur at the beginning of the file | |
| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | |
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | |
| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | |
| F541 | FStringMissingPlaceholders | f-string without any placeholders | |
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | |
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | |
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | |
| F622 | TwoStarredExpressions | Two starred expressions in assignment | |
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | |
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | |
| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | |
| 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 | |
| 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: `...` | |
| F821 | UndefinedName | Undefined name `...` | |
| F822 | UndefinedExport | Undefined name `...` in `__all__` | |
| F823 | UndefinedLocal | Local variable `...` referenced before assignment | |
| F831 | DuplicateArgumentName | Duplicate argument name in function definition | |
| F841 | UnusedVariable | Local variable `...` is assigned to but never used | |
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | |
### pycodestyle (error)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | |
| E501 | LineTooLong | Line too long (89 > 88 characters) | |
| E711 | NoneComparison | Comparison to `None` should be `cond is None` | |
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | |
| E713 | NotInTest | Test for membership should be `not in` | |
| E714 | NotIsTest | Test for object identity should be `is not` | |
| E721 | TypeComparison | Do not compare types, use `isinstance()` | |
| E722 | DoNotUseBareExcept | Do not use bare `except` | |
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | |
| E741 | AmbiguousVariableName | Ambiguous variable name: `...` | |
| E742 | AmbiguousClassName | Ambiguous class name: `...` | |
| 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
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| D100 | PublicModule | Missing docstring in public module | |
| D101 | PublicClass | Missing docstring in public class | |
| D102 | PublicMethod | Missing docstring in public method | |
| D103 | PublicFunction | Missing docstring in public function | |
| D104 | PublicPackage | Missing docstring in public package | |
| D105 | MagicMethod | Missing docstring in magic method | |
| D106 | PublicNestedClass | Missing docstring in public nested class | |
| D107 | PublicInit | Missing docstring in `__init__` | |
| D200 | FitsOnOneLine | One-line docstring should fit on one line | |
| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | 🛠 |
| 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 | 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 | 🛠 |
| 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") | 🛠 |
| D300 | UsesTripleQuotes | Use """triple double quotes""" | |
| D400 | EndsInPeriod | First line should end with a period | |
| D402 | NoSignature | First line should not be the function's signature | |
| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | |
| D404 | NoThisPrefix | First word of the docstring should not be `This` | |
| 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") | 🛠 |
| 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") | 🛠 |
| D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` | |
| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | |
| D419 | NonEmpty | Docstring is empty | |
### pyupgrade
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | 🛠 |
| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | 🛠 |
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | 🛠 |
| U004 | UselessObjectInheritance | Class `...` inherits from object | 🛠 |
| U005 | DeprecatedUnittestAlias | `assertEquals` is deprecated, use `assertEqual` instead | 🛠 |
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | 🛠 |
| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | 🛠 |
| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | 🛠 |
### pep8-naming
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| N801 | InvalidClassName | Class name `...` should use CapWords convention | |
| N802 | InvalidFunctionName | Function name `...` should be lowercase | |
| 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` | |
| 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 `...` | |
| N817 | CamelcaseImportedAsAcronym | Camelcase `...` imported as acronym `...` | |
### flake8-comprehensions
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| C400 | UnnecessaryGeneratorList | Unnecessary generator (rewrite as a `list` comprehension) | |
| C401 | UnnecessaryGeneratorSet | Unnecessary generator (rewrite as a `set` comprehension) | |
| C402 | UnnecessaryGeneratorDict | Unnecessary generator (rewrite as a `dict` comprehension) | |
| C403 | UnnecessaryListComprehensionSet | Unnecessary `list` comprehension (rewrite as a `set` comprehension) | |
| C404 | UnnecessaryListComprehensionDict | Unnecessary `list` comprehension (rewrite as a `dict` comprehension) | |
| C405 | UnnecessaryLiteralSet | Unnecessary `(list\|tuple)` literal (rewrite as a `set` literal) | |
| C406 | UnnecessaryLiteralDict | Unnecessary `(list\|tuple)` literal (rewrite as a `dict` literal) | |
| C408 | UnnecessaryCollectionCall | Unnecessary `(dict\|list\|tuple)` call (rewrite as a literal) | |
| C409 | UnnecessaryLiteralWithinTupleCall | Unnecessary `(list\|tuple)` literal passed to `tuple()` (remove the outer call to `tuple()`) | |
| C410 | UnnecessaryLiteralWithinListCall | Unnecessary `(list\|tuple)` literal passed to `list()` (rewrite as a `list` literal) | |
| C411 | UnnecessaryListCall | Unnecessary `list` call (remove the outer call to `list()`) | |
| 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)()`) | |
| 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. | |
| 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()` | 🛠 |
| 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
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin | |
| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin | |
| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin | |
### flake8-print
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| T201 | PrintFound | `print` found | 🛠 |
| T203 | PPrintFound | `pprint` found | 🛠 |
### Meta rules
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| M001 | UnusedNOQA | Unused `noqa` directive | 🛠 |
## Editor Integrations
### PyCharm
ruff can be installed as an [External Tool](https://www.jetbrains.com/help/pycharm/configuring-third-party-tools.html)
Ruff can be installed as an [External Tool](https://www.jetbrains.com/help/pycharm/configuring-third-party-tools.html)
in PyCharm. Open the Preferences pane, then navigate to "Tools", then "External Tools". From there,
add a new tool with the following configuration:
![Install ruff as an External Tool](https://user-images.githubusercontent.com/1309177/193155720-336e43f0-1a8d-46b4-bc12-e60f9ae01f7e.png)
![Install Ruff as an External Tool](https://user-images.githubusercontent.com/1309177/193155720-336e43f0-1a8d-46b4-bc12-e60f9ae01f7e.png)
ruff should then appear as a runnable action:
Ruff should then appear as a runnable action:
![ruff as a runnable action](https://user-images.githubusercontent.com/1309177/193156026-732b0aaf-3dd9-4549-9b4d-2de6d2168a33.png)
![Ruff as a runnable action](https://user-images.githubusercontent.com/1309177/193156026-732b0aaf-3dd9-4549-9b4d-2de6d2168a33.png)
### GitHub Actions
GitHub Actions has everything you need to run ruff out-of-the-box:
GitHub Actions has everything you need to run Ruff out-of-the-box:
```yaml
name: CI
@@ -333,13 +452,175 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install ruff
- name: Run ruff
- name: Run Ruff
run: ruff .
```
## FAQ
### Is Ruff compatible with Black?
Yes. Ruff is compatible with [Black](https://github.com/psf/black) out-of-the-box, as long as
the `line-length` setting is consistent between the two.
As a project, Ruff is designed to be used alongside Black and, as such, will defer implementing
stylistic lint rules that are obviated by autoformatting.
### How does Ruff compare to Flake8?
Ruff can be used as a (near) drop-in replacement for Flake8 when used (1) without or with a small
number of plugins, (2) alongside Black, and (3) on Python 3 code.
Under those conditions Ruff is missing 14 rules related to string `.format` calls, 1 rule related
to docstring parsing, and 1 rule related to redefined variables.
Ruff re-implements some of the most popular Flake8 plugins and related code quality tools natively,
including:
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`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-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (9/32)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
Beyond rule-set parity, Ruff suffers from the following limitations vis-à-vis Flake8:
1. Ruff does not yet support a few Python 3.9 and 3.10 language features, including structural
pattern matching and parenthesized context managers.
2. Flake8 has a plugin architecture and supports writing custom lint rules. (To date, popular Flake8
plugins have been re-implemented within Ruff directly.)
### Which tools does Ruff replace?
Today, Ruff can be used to replace Flake8 when used with any of the following plugins:
- [`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-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (9/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).
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.
### Do I need to install Rust to use Ruff?
Nope! Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
```shell
pip install ruff
```
Ruff ships with wheels for all major platforms, which enables `pip` to install Ruff without relying
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/283) for more.
### Does Ruff support NumPy- or Google-style docstrings?
Yes! To enable a specific docstring convention, start by enabling all `pydocstyle` error codes, and
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`:
```toml
[tool.ruff]
extend-select = [
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D200",
"D201",
"D202",
"D204",
"D205",
"D206",
"D207",
"D208",
"D209",
"D210",
"D211",
"D214",
"D215",
"D300",
"D400",
"D402",
"D403",
"D404",
"D405",
"D406",
"D407",
"D408",
"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",
]
```
## Development
ruff is written in Rust (1.63.0). You'll need to install the [Rust toolchain](https://www.rust-lang.org/tools/install)
Ruff is written in Rust (1.63.0). You'll need to install the [Rust toolchain](https://www.rust-lang.org/tools/install)
for development.
Assuming you have `cargo` installed, you can run:
@@ -351,13 +632,13 @@ cargo clippy
cargo test
```
## Deployment
## Releases
ruff is distributed on [PyPI](https://pypi.org/project/ruff/), and published via [`maturin`](https://github.com/PyO3/maturin).
Ruff is distributed on [PyPI](https://pypi.org/project/ruff/), and published via [`maturin`](https://github.com/PyO3/maturin).
See: `.github/workflows/release.yaml`.
## Benchmarking
## Benchmarks
First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase,
which makes it a good target for benchmarking.
@@ -434,9 +715,9 @@ hyperfine --ignore-failure --warmup 5 \
In order, these evaluate:
- ruff
- Ruff
- Pylint
- PyFlakes
- Pyflakes
- autoflake
- pycodestyle
- Flake8

View File

@@ -1,26 +1,29 @@
/// Generate a Markdown-compatible table of supported lint rules.
//! Generate a Markdown-compatible table of supported lint rules.
use strum::IntoEnumIterator;
use ruff::checks::{CheckCode, DEFAULT_CHECK_CODES};
use ruff::checks::{CheckCategory, CheckCode};
fn main() {
println!("| Code | Name | Message | | |");
println!("| ---- | ---- | ------- | --- | --- |");
for check_code in CheckCode::iter() {
let check_kind = check_code.kind();
let default_token = if DEFAULT_CHECK_CODES.contains(&check_code) {
""
} else {
""
};
let fix_token = if check_kind.fixable() { "🛠" } else { "" };
println!(
"| {} | {} | {} | {} | {} |",
check_kind.code().as_ref(),
check_kind.as_ref(),
check_kind.body().replace("|", r"\|"),
default_token,
fix_token
);
for check_category in CheckCategory::iter() {
println!("### {}", check_category.title());
println!();
println!("| Code | Name | Message | Fix |");
println!("| ---- | ---- | ------- | --- |");
for check_code in CheckCode::iter() {
if check_code.category() == check_category {
let check_kind = check_code.kind();
let fix_token = if check_kind.fixable() { "🛠" } else { "" };
println!(
"| {} | {} | {} | {} |",
check_kind.code().as_ref(),
check_kind.as_ref(),
check_kind.summary().replace("|", r"\|"),
fix_token
);
}
}
println!();
}
}

View File

@@ -1,4 +1,5 @@
/// Print the AST for a given Python file.
//! Print the AST for a given Python file.
use std::path::PathBuf;
use anyhow::Result;

View File

@@ -1,4 +1,5 @@
/// Print the token stream for a given Python file.
//! Print the token stream for a given Python file.
use std::path::PathBuf;
use anyhow::Result;

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

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)

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

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

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

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

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()

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

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

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

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

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

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

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

@@ -0,0 +1,2 @@
x = [1, 2, 3]
list([i for i in x])

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

@@ -0,0 +1,4 @@
x = [2, 3, 1]
list(sorted(x))
reversed(sorted(x))
reversed(sorted(x, reverse=True))

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

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

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

@@ -0,0 +1,6 @@
x = [1, 2, 3]
[i for i in x]
{i for i in x}
[i for i in x if i > 1]
[i for i in x for j in x]

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

@@ -0,0 +1,6 @@
nums = [1, 2, 3]
map(lambda x: x + 1, nums)
map(lambda x: str(x), nums)
list(map(lambda x: x * 2, nums))
set(map(lambda x: x % 2 == 0, nums))
dict(map(lambda v: (v, v**2), nums))

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

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

View File

@@ -61,7 +61,7 @@ Y = TypeVar("Y", bound="Dict")
Z = TypeVar("Z", "List", "Set")
a = list["Fruit"]
b = Union["Nut", None]
b = Union["""Nut""", None]
c = cast("Vegetable", b)
Field = lambda default=MISSING: field(default=default)

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

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

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

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

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

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

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

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

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,4 +1,3 @@
from __future__ import print_function
"""Docstring"""
from __future__ import absolute_import

View File

@@ -122,3 +122,12 @@ class PEP593Test:
dict["foo", "bar"], # Expected to fail as undefined.
123,
]
def in_ipython_notebook() -> bool:
try:
# autoimported by notebooks
get_ipython() # type: ignore[name-defined]
except NameError:
return False # not in notebook
return True

View File

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

34
resources/test/fixtures/N801.py vendored Normal file
View File

@@ -0,0 +1,34 @@
class bad:
pass
class _bad:
pass
class bad_class:
pass
class Bad_Class:
pass
class BAD_CLASS:
pass
class Good:
pass
class _Good:
pass
class GoodClass:
pass
class GOOD:
pass

26
resources/test/fixtures/N802.py vendored Normal file
View File

@@ -0,0 +1,26 @@
def Bad():
pass
def _Bad():
pass
def BAD():
pass
def BAD_FUNC():
pass
def good():
pass
def _good():
pass
def good_func():
pass

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

@@ -0,0 +1,7 @@
def func(a, A):
return a, A
class Class:
def method(self, a, A):
return a, A

19
resources/test/fixtures/N804.py vendored Normal file
View File

@@ -0,0 +1,19 @@
class Class:
@classmethod
def bad_class_method(this):
pass
@classmethod
def good_class_method(cls):
pass
def method(self):
pass
@staticmethod
def static_method(x):
return x
def func(x):
return x

26
resources/test/fixtures/N805.py vendored Normal file
View File

@@ -0,0 +1,26 @@
import random
class Class:
def bad_method(this):
pass
if random.random(0, 2) == 0:
def extra_bad_method(this):
pass
def good_method(self):
pass
@classmethod
def class_method(cls):
pass
@staticmethod
def static_method(x):
return x
def func(x):
return x

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

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

View File

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

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,108 @@
"""A one line summary of the module or program, terminated by a period.
Leave one blank line. The rest of this docstring should contain an
overall description of the module or program. Optionally, it may also
contain a brief description of exported classes and functions and/or usage
examples.
Typical usage example:
foo = ClassFoo()
bar = foo.FunctionBar()
"""
# above: "2.8.2 Modules" section example
# https://google.github.io/styleguide/pyguide.html#382-modules
# Examples from the official "Google Python Style Guide" documentation:
# * As HTML: https://google.github.io/styleguide/pyguide.html
# * Source Markdown:
# https://github.com/google/styleguide/blob/gh-pages/pyguide.md
import os
from .expected import Expectation
expectation = Expectation()
expect = expectation.expect
# module docstring expected violations:
expectation.expected.add((
os.path.normcase(__file__),
"D213: Multi-line docstring summary should start at the second line"))
# "3.8.3 Functions and Methods" section example
# https://google.github.io/styleguide/pyguide.html#383-functions-and-methods
@expect("D213: Multi-line docstring summary should start at the second line",
arg_count=3)
@expect("D401: First line should be in imperative mood "
"(perhaps 'Fetch', not 'Fetches')", arg_count=3)
@expect("D406: Section name should end with a newline "
"('Raises', not 'Raises:')", arg_count=3)
@expect("D406: Section name should end with a newline "
"('Returns', not 'Returns:')", arg_count=3)
@expect("D407: Missing dashed underline after section ('Raises')", arg_count=3)
@expect("D407: Missing dashed underline after section ('Returns')",
arg_count=3)
@expect("D413: Missing blank line after last section ('Raises')", arg_count=3)
def fetch_bigtable_rows(big_table, keys, other_silly_variable=None):
"""Fetches rows from a Bigtable.
Retrieves rows pertaining to the given keys from the Table instance
represented by big_table. Silly things may happen if
other_silly_variable is not None.
Args:
big_table: An open Bigtable Table instance.
keys: A sequence of strings representing the key of each table row
to fetch.
other_silly_variable: Another optional variable, that has a much
longer name than the other args, and which does nothing.
Returns:
A dict mapping keys to the corresponding table row data
fetched. Each row is represented as a tuple of strings. For
example:
{'Serak': ('Rigel VII', 'Preparer'),
'Zim': ('Irk', 'Invader'),
'Lrrr': ('Omicron Persei 8', 'Emperor')}
If a key from the keys argument is missing from the dictionary,
then that row was not found in the table.
Raises:
IOError: An error occurred accessing the bigtable.Table object.
"""
# "3.8.4 Classes" section example
# https://google.github.io/styleguide/pyguide.html#384-classes
@expect("D203: 1 blank line required before class docstring (found 0)")
@expect("D213: Multi-line docstring summary should start at the second line")
@expect("D406: Section name should end with a newline "
"('Attributes', not 'Attributes:')")
@expect("D407: Missing dashed underline after section ('Attributes')")
@expect("D413: Missing blank line after last section ('Attributes')")
class SampleClass:
"""Summary of class here.
Longer class information....
Longer class information....
Attributes:
likes_spam: A boolean indicating if we like SPAM or not.
eggs: An integer count of the eggs we have laid.
"""
@expect("D401: First line should be in imperative mood "
"(perhaps 'Init', not 'Inits')", arg_count=2)
def __init__(self, likes_spam=False):
"""Inits SampleClass with blah."""
if self: # added to avoid NameError when run via @expect decorator
self.likes_spam = likes_spam
self.eggs = 0
@expect("D401: First line should be in imperative mood "
"(perhaps 'Perform', not 'Performs')", arg_count=1)
def public_method(self):
"""Performs operation blah."""

View File

@@ -0,0 +1,163 @@
"""This is the docstring for the example.py module. Modules names should
have short, all-lowercase names. The module name may have underscores if
this improves readability.
Every module should have a docstring at the very top of the file. The
module's docstring may extend over multiple lines. If your docstring does
extend over multiple lines, the closing three quotation marks must be on
a line by itself, preferably preceded by a blank line.
"""
# Example source file from the official "numpydoc docstring guide"
# documentation (with the modification of commenting out all the original
# ``import`` lines, plus adding this note and ``Expectation`` code):
# * As HTML: https://numpydoc.readthedocs.io/en/latest/example.html
# * Source Python:
# https://github.com/numpy/numpydoc/blob/master/doc/example.py
# from __future__ import division, absolute_import, print_function
#
# import os # standard library imports first
#
# Do NOT import using *, e.g. from numpy import *
#
# Import the module using
#
# import numpy
#
# instead or import individual functions as needed, e.g
#
# from numpy import array, zeros
#
# If you prefer the use of abbreviated module names, we suggest the
# convention used by NumPy itself::
#
# import numpy as np
# import matplotlib as mpl
# import matplotlib.pyplot as plt
#
# These abbreviated names are not to be used in docstrings; users must
# be able to paste and execute docstrings after importing only the
# numpy module itself, unabbreviated.
import os
from .expected import Expectation
expectation = Expectation()
expect = expectation.expect
# module docstring expected violations:
expectation.expected.add((
os.path.normcase(__file__),
"D205: 1 blank line required between summary line and description "
"(found 0)"))
expectation.expected.add((
os.path.normcase(__file__),
"D213: Multi-line docstring summary should start at the second line"))
expectation.expected.add((
os.path.normcase(__file__),
"D400: First line should end with a period (not 'd')"))
expectation.expected.add((
os.path.normcase(__file__),
"D404: First word of the docstring should not be `This`"))
expectation.expected.add((
os.path.normcase(__file__),
"D415: First line should end with a period, question mark, or exclamation "
"point (not 'd')"))
@expect("D213: Multi-line docstring summary should start at the second line",
arg_count=3)
@expect("D401: First line should be in imperative mood; try rephrasing "
"(found 'A')", arg_count=3)
@expect("D413: Missing blank line after last section ('Examples')",
arg_count=3)
def foo(var1, var2, long_var_name='hi'):
r"""A one-line summary that does not use variable names.
Several sentences providing an extended description. Refer to
variables using back-ticks, e.g. `var`.
Parameters
----------
var1 : array_like
Array_like means all those objects -- lists, nested lists, etc. --
that can be converted to an array. We can also refer to
variables like `var1`.
var2 : int
The type above can either refer to an actual Python type
(e.g. ``int``), or describe the type of the variable in more
detail, e.g. ``(N,) ndarray`` or ``array_like``.
long_var_name : {'hi', 'ho'}, optional
Choices in brackets, default first when optional.
Returns
-------
type
Explanation of anonymous return value of type ``type``.
describe : type
Explanation of return value named `describe`.
out : type
Explanation of `out`.
type_without_description
Other Parameters
----------------
only_seldom_used_keywords : type
Explanation
common_parameters_listed_above : type
Explanation
Raises
------
BadException
Because you shouldn't have done that.
See Also
--------
numpy.array : Relationship (optional).
numpy.ndarray : Relationship (optional), which could be fairly long, in
which case the line wraps here.
numpy.dot, numpy.linalg.norm, numpy.eye
Notes
-----
Notes about the implementation algorithm (if needed).
This can have multiple paragraphs.
You may include some math:
.. math:: X(e^{j\omega } ) = x(n)e^{ - j\omega n}
And even use a Greek symbol like :math:`\omega` inline.
References
----------
Cite the relevant literature, e.g. [1]_. You may also cite these
references in the notes section above.
.. [1] O. McNoleg, "The integration of GIS, remote sensing,
expert systems and adaptive co-kriging for environmental habitat
modelling of the Highland Haggis using object-oriented, fuzzy-logic
and neural-network techniques," Computers & Geosciences, vol. 22,
pp. 585-588, 1996.
Examples
--------
These are written in doctest format, and should illustrate how to
use the function.
>>> a = [1, 2, 3]
>>> print([x + 3 for x in a])
[4, 5, 6]
>>> print("a\nb")
a
b
"""
# After closing class docstring, there should be one blank line to
# separate following codes (according to PEP257).
# But for function, method and module, there should be no blank lines
# after closing the docstring.
pass

View File

@@ -5,3 +5,6 @@ extend-exclude = [
"migrations",
"directory/also_excluded.py",
]
per-file-ignores = [
"__init__.py:F401",
]

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

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

19
scripts/.flake8 Normal file
View File

@@ -0,0 +1,19 @@
[flake8]
exclude =
# Defaults
.svn,
CVS,
.bzr,
.hg,
.git,
__pycache__,
.tox,
.idea,
.mypy_cache,
.venv,
node_modules,
# Custom
_state_machine.py,
test_fstring.py,
bad_coding2.py,
badsyntax_*.py

23
setup.py Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -121,45 +121,78 @@ pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
/// Struct used to efficiently slice source code at (row, column) Locations.
pub struct SourceCodeLocator<'a> {
content: &'a str,
offsets: Vec<usize>,
initialized: bool,
offsets: Vec<Vec<usize>>,
}
impl<'a> SourceCodeLocator<'a> {
pub fn new(content: &'a str) -> Self {
SourceCodeLocator {
content,
offsets: vec![],
initialized: false,
offsets: Self::compute_offsets(content),
}
}
pub fn slice_source_code_at(&mut self, location: &Location) -> &'a str {
if !self.initialized {
let mut offset = 0;
for i in self.content.lines() {
self.offsets.push(offset);
offset += i.len();
offset += 1;
fn compute_offsets(content: &str) -> Vec<Vec<usize>> {
let mut offsets = vec![];
let mut offset = 0;
for line in 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 + char.len_utf8();
}
self.initialized = true;
line_offsets.push(offset + newline);
offsets.push(line_offsets);
offset += newline + 1;
}
let offset = self.offsets[location.row() - 1] + location.column() - 1;
offsets.push(vec![offset]);
offsets
}
pub fn slice_source_code_at(&self, location: &Location) -> &'a str {
let offset = self.offsets[location.row() - 1][location.column() - 1];
&self.content[offset..]
}
pub fn slice_source_code_range(&mut self, range: &Range) -> &'a str {
if !self.initialized {
let mut offset = 0;
for i in self.content.lines() {
self.offsets.push(offset);
offset += i.len();
offset += 1;
}
self.initialized = true;
}
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;
pub fn slice_source_code_range(&self, range: &Range) -> &'a str {
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(
&self,
outer: &Range,
inner: &Range,
) -> (&'a str, &'a str, &'a str) {
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],
)
}
}
#[cfg(test)]
mod tests {
use super::SourceCodeLocator;
#[test]
fn source_code_locator_init() {
let content = "# \u{4e9c}\nclass Foo:\n \"\"\".\"\"\"";
let locator = SourceCodeLocator::new(content);
assert_eq!(locator.offsets.len(), 4);
assert_eq!(locator.offsets[0], [0, 1, 2, 5]);
assert_eq!(locator.offsets[1], [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
assert_eq!(
locator.offsets[2],
[17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
);
assert_eq!(locator.offsets[3], [29]);
}
}

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,10 +1,11 @@
use crate::ast::helpers::match_name_or_attr;
use rustpython_parser::ast::{
Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler,
ExcepthandlerKind, Expr, ExprContext, ExprKind, Keyword, MatchCase, Operator, Pattern,
PatternKind, Stmt, StmtKind, Unaryop, Withitem,
};
use crate::ast::helpers::match_name_or_attr;
pub trait Visitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
walk_stmt(self, stmt);

View File

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

View File

@@ -1,11 +1,11 @@
use std::fs;
use std::path::Path;
use std::collections::BTreeSet;
use anyhow::Result;
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 {
@@ -14,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 {
@@ -24,51 +35,59 @@ impl From<bool> for Mode {
}
/// Auto-fix errors in a file, and write the fixed source code to disk.
pub fn fix_file(checks: &mut [Check], contents: &str, path: &Path) -> Result<()> {
pub fn fix_file(checks: &mut [Check], contents: &str) -> Option<String> {
if checks.iter().all(|check| check.fix.is_none()) {
return Ok(());
return None;
}
let output = apply_fixes(
Some(apply_fixes(
checks.iter_mut().filter_map(|check| check.fix.as_mut()),
contents,
);
fs::write(path, output).map_err(|e| e.into())
))
}
/// Apply a series of fixes.
fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) -> String {
let lines: Vec<&str> = contents.lines().collect();
let mut output = "".to_string();
let mut last_pos: Location = Location::new(0, 0);
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('\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() - 1]);
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() - 1..fix.patch.location.column() - 1],
);
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;
}
@@ -95,7 +114,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<()> {
@@ -111,9 +131,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, 9),
end_location: Location::new(1, 15),
},
applied: false,
}];
let actual = apply_fixes(
@@ -135,9 +157,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, 8),
end_location: Location::new(1, 16),
},
applied: false,
}];
let actual = apply_fixes(
@@ -160,15 +184,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, 8),
end_location: Location::new(1, 17),
},
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, 17),
end_location: Location::new(1, 24),
},
applied: false,
},
];
@@ -192,15 +220,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, 8),
end_location: Location::new(1, 16),
},
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, 10),
end_location: Location::new(1, 12),
},
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(), 1),
Location::new(stmt.end_location.unwrap().row() + 1, 1),
))
}
}

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

View File

@@ -1,3 +1,9 @@
// cacache uses asyncd-std which has no wasm support, so currently no caching support on wasm
#![cfg_attr(
target_family = "wasm",
allow(unused_imports, unused_variables, dead_code)
)]
use std::collections::hash_map::DefaultHasher;
use std::fs::{create_dir_all, File, Metadata};
use std::hash::{Hash, Hasher};
@@ -5,6 +11,7 @@ use std::io::Write;
use std::path::Path;
use anyhow::Result;
#[cfg(not(target_family = "wasm"))]
use cacache::Error::EntryNotFound;
use filetime::FileTime;
use log::error;
@@ -107,6 +114,7 @@ pub fn get(
return None;
};
#[cfg(not(target_family = "wasm"))] // cacache needs async-std which doesn't support wasm
match cacache::read_sync(cache_dir(), cache_key(path, settings, autofix)) {
Ok(encoded) => match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
@@ -137,12 +145,14 @@ pub fn set(
return;
};
#[cfg(not(target_family = "wasm"))] // modification date not supported on wasm
let check_result = CheckResultRef {
metadata: &CacheMetadata {
mtime: FileTime::from_last_modification_time(metadata).unix_seconds(),
},
messages,
};
#[cfg(not(target_family = "wasm"))] // cacache needs async-std which doesn't support wasm
if let Err(e) = cacache::write_sync(
cache_dir(),
cache_key(path, settings, autofix),

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;
@@ -165,16 +168,11 @@ pub fn check_lines(
end_location: Location::new(row + 1, end + 1),
},
);
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 + 1),
Location::new(row + 1, lines[row].chars().count() + 1),
));
}
line_checks.push(check);
}
@@ -184,41 +182,32 @@ pub fn check_lines(
let mut valid_codes = vec![];
for code in codes {
if !matches.contains(&code) {
invalid_codes.push(code);
invalid_codes.push(code.to_string());
} else {
valid_codes.push(code);
valid_codes.push(code.to_string());
}
}
if !invalid_codes.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(invalid_codes.join(", "))),
CheckKind::UnusedNOQA(Some(invalid_codes)),
Range {
location: Location::new(row + 1, start + 1),
end_location: Location::new(row + 1, end + 1),
},
);
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 + 1),
Location::new(row + 1, lines[row].chars().count() + 1),
));
} 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 + 1),
Location::new(row + 1, lines[row].chars().count() + 1),
));
}
}
line_checks.push(check);

124
src/check_tokens.rs Normal file
View File

@@ -0,0 +1,124 @@
//! Lint rules based on token traversal.
use rustpython_ast::Location;
use rustpython_parser::lexer::{LexResult, Tok};
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::Range;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::Settings;
// See: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
const VALID_ESCAPE_SEQUENCES: &[char; 23] = &[
'\n', '\\', '\'', '"', 'a', 'b', 'f', 'n', 'r', 't', 'v', '0', '1', '2', '3', '4', '5', '6',
'7', 'x', // Escape sequences only recognized in string literals
'N', 'u', 'U',
];
/// Return the quotation markers used for a String token.
fn extract_quote(text: &str) -> &str {
if text.len() >= 3 {
let triple = &text[text.len() - 3..];
if triple == "'''" || triple == "\"\"\"" {
return triple;
}
}
if !text.is_empty() {
let single = &text[text.len() - 1..];
if single == "'" || single == "\"" {
return single;
}
}
panic!("Unable to find quotation mark for String token.")
}
/// W605
fn invalid_escape_sequence(
locator: &SourceCodeLocator,
start: &Location,
end: &Location,
) -> Vec<Check> {
let mut checks = vec![];
let text = locator.slice_source_code_range(&Range {
location: *start,
end_location: *end,
});
// Determine whether the string is single- or triple-quoted.
let quote = extract_quote(text);
let quote_pos = text.find(quote).unwrap();
let prefix = text[..quote_pos].to_lowercase();
let body = &text[(quote_pos + quote.len())..(text.len() - quote.len())];
if !prefix.contains('r') {
let mut col_offset = 0;
let mut row_offset = 0;
let mut in_escape = false;
let mut chars = body.chars();
let mut current = chars.next();
let mut next = chars.next();
while let (Some(current_char), Some(next_char)) = (current, next) {
// If we see an escaped backslash, avoid treating the character _after_ the
// escaped backslash as itself an escaped character.
if in_escape {
in_escape = false;
} else {
in_escape = current_char == '\\' && next_char == '\\';
if current_char == '\\' && !VALID_ESCAPE_SEQUENCES.contains(&next_char) {
// Compute the location of the escape sequence by offsetting the location of the
// string token by the characters we've seen thus far.
let location = if row_offset == 0 {
Location::new(
start.row() + row_offset,
start.column() + prefix.len() + quote.len() + col_offset,
)
} else {
Location::new(start.row() + row_offset, col_offset + 1)
};
let end_location = Location::new(location.row(), location.column() + 1);
checks.push(Check::new(
CheckKind::InvalidEscapeSequence(next_char),
Range {
location,
end_location,
},
))
}
}
// Track the offset from the start position as we iterate over the body.
if current_char == '\n' {
col_offset = 0;
row_offset += 1;
} else {
col_offset += 1;
}
current = next;
next = chars.next();
}
}
checks
}
pub fn check_tokens(
checks: &mut Vec<Check>,
contents: &str,
tokens: &[LexResult],
settings: &Settings,
) {
// TODO(charlie): Use a shared SourceCodeLocator between this site and the AST traversal.
let locator = SourceCodeLocator::new(contents);
let enforce_invalid_escape_sequence = settings.enabled.contains(&CheckCode::W605);
for (start, tok, end) in tokens.iter().flatten() {
if enforce_invalid_escape_sequence {
if matches!(tok, Tok::String { .. }) {
checks.extend(invalid_escape_sequence(&locator, start, end));
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -17,12 +17,18 @@ use crate::RawSettings;
pub struct Cli {
#[arg(required = true)]
pub files: Vec<PathBuf>,
/// Path to the `pyproject.toml` file to use for configuration.
#[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,
@@ -78,6 +84,9 @@ pub struct Cli {
// TODO(charlie): This should be a sub-command.
#[arg(long, hide = true)]
pub autoformat: bool,
/// The name of the file when passing it through stdin.
#[arg(long)]
pub stdin_filename: Option<String>,
}
pub enum Warnable {
@@ -97,9 +106,9 @@ 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: &Vec<CheckCode>,
cli_ignore: &Vec<CheckCode>,
cli_extend_ignore: &Vec<CheckCode>,
codes: &[CheckCode],
cli_ignore: &[CheckCode],
cli_extend_ignore: &[CheckCode],
pyproject_settings: &RawSettings,
pyproject_path: &Option<PathBuf>,
) {

View File

@@ -115,7 +115,7 @@ impl SourceGenerator {
Ok(())
}
fn unparse_stmt<U>(&mut self, ast: &Stmt<U>) -> fmt::Result {
pub fn unparse_stmt<U>(&mut self, ast: &Stmt<U>) -> fmt::Result {
macro_rules! statement {
($body:block) => {{
self.newline()?;

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

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

View File

@@ -0,0 +1,82 @@
//! Extract docstrings from an AST.
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
use crate::visibility::{Modifier, VisibleScope};
/// Extract a docstring from a function or class body.
pub fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
if let Some(stmt) = suite.first() {
if let StmtKind::Expr { value } = &stmt.node {
if matches!(
&value.node,
ExprKind::Constant {
value: Constant::Str(_),
..
}
) {
return Some(value);
}
}
}
None
}
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub fn extract<'a>(
scope: &VisibleScope,
stmt: &'a Stmt,
body: &'a [Stmt],
kind: &Documentable,
) -> Definition<'a> {
let expr = docstring_from(body);
match kind {
Documentable::Function => match scope {
VisibleScope {
modifier: Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Function(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
..
} => Definition {
kind: DefinitionKind::Method(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedFunction(stmt),
docstring: expr,
},
},
Documentable::Class => match scope {
VisibleScope {
modifier: Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Class(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
},
}
}

71
src/docstrings/google.rs Normal file
View File

@@ -0,0 +1,71 @@
//! Abstractions for Google-style docstrings.
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
pub(crate) static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"Args",
"Arguments",
"Attention",
"Attributes",
"Caution",
"Danger",
"Error",
"Example",
"Examples",
"Hint",
"Important",
"Keyword Args",
"Keyword Arguments",
"Methods",
"Note",
"Notes",
"Return",
"Returns",
"Raises",
"References",
"See Also",
"Tip",
"Todo",
"Warning",
"Warnings",
"Warns",
"Yield",
"Yields",
])
});
pub(crate) static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"args",
"arguments",
"attention",
"attributes",
"caution",
"danger",
"error",
"example",
"examples",
"hint",
"important",
"keyword args",
"keyword arguments",
"methods",
"note",
"notes",
"return",
"returns",
"raises",
"references",
"see also",
"tip",
"todo",
"warning",
"warnings",
"warns",
"yield",
"yields",
])
});

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

@@ -0,0 +1,42 @@
use rustpython_ast::{Expr, Location};
use crate::ast::types::Range;
use crate::check_ast::Checker;
pub const TRIPLE_QUOTE_PREFIXES: &[&str] = &[
"ur\"\"\"", "ur'''", "u\"\"\"", "u'''", "r\"\"\"", "r'''", "\"\"\"", "'''",
];
pub const SINGLE_QUOTE_PREFIXES: &[&str] = &["ur\"", "ur'", "u\"", "u'", "r\"", "r'", "\"", "'"];
/// Extract the leading words from a line of text.
pub fn leading_words(line: &str) -> String {
line.trim()
.chars()
.take_while(|char| char.is_alphanumeric() || char.is_whitespace())
.collect()
}
/// Extract the leading whitespace from a line of text.
pub fn leading_space(line: &str) -> String {
line.chars()
.take_while(|char| char.is_whitespace())
.collect()
}
/// Extract the leading indentation from a docstring.
pub fn indentation<'a>(checker: &'a Checker, docstring: &Expr) -> &'a str {
let range = Range::from_located(docstring);
checker.get_locator().slice_source_code_range(&Range {
location: Location::new(range.location.row(), 1),
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;

41
src/docstrings/numpy.rs Normal file
View File

@@ -0,0 +1,41 @@
//! Abstractions for NumPy-style docstrings.
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"short summary",
"extended summary",
"parameters",
"returns",
"yields",
"other parameters",
"raises",
"see also",
"notes",
"references",
"examples",
"attributes",
"methods",
])
});
pub(crate) static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"Short Summary",
"Extended Summary",
"Parameters",
"Returns",
"Yields",
"Other Parameters",
"Raises",
"See Also",
"Notes",
"References",
"Examples",
"Attributes",
"Methods",
])
});

View File

@@ -0,0 +1,99 @@
use crate::docstrings::helpers;
use crate::docstrings::styles::SectionStyle;
#[derive(Debug)]
pub(crate) struct SectionContext<'a> {
pub(crate) section_name: String,
pub(crate) previous_line: &'a str,
pub(crate) line: &'a str,
pub(crate) following_lines: &'a [&'a str],
pub(crate) is_last_section: bool,
pub(crate) original_index: usize,
}
fn suspected_as_section(line: &str, style: &SectionStyle) -> bool {
style
.lowercase_section_names()
.contains(&helpers::leading_words(line).to_lowercase().as_str())
}
/// Check if the suspected context is really a section header.
fn is_docstring_section(context: &SectionContext) -> bool {
let section_name_suffix = context
.line
.trim()
.strip_prefix(&context.section_name)
.unwrap()
.trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
if !this_looks_like_a_section_name {
return false;
}
let prev_line = context.previous_line.trim();
let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
.into_iter()
.any(|char| prev_line.ends_with(char));
let prev_line_looks_like_end_of_paragraph =
prev_line_ends_with_punctuation || prev_line.is_empty();
if !prev_line_looks_like_end_of_paragraph {
return false;
}
true
}
/// Extract all `SectionContext` values from a docstring.
pub(crate) fn section_contexts<'a>(
lines: &'a [&'a str],
style: &SectionStyle,
) -> Vec<SectionContext<'a>> {
let suspected_section_indices: Vec<usize> = lines
.iter()
.enumerate()
.filter_map(|(lineno, line)| {
if lineno > 0 && suspected_as_section(line, style) {
Some(lineno)
} else {
None
}
})
.collect();
let mut contexts = vec![];
for lineno in suspected_section_indices {
let context = SectionContext {
section_name: helpers::leading_words(lines[lineno]),
previous_line: lines[lineno - 1],
line: lines[lineno],
following_lines: &lines[lineno + 1..],
original_index: lineno,
is_last_section: false,
};
if is_docstring_section(&context) {
contexts.push(context);
}
}
let mut truncated_contexts = vec![];
let mut end: Option<usize> = None;
for context in contexts.into_iter().rev() {
let next_end = context.original_index;
truncated_contexts.push(SectionContext {
section_name: context.section_name,
previous_line: context.previous_line,
line: context.line,
following_lines: if let Some(end) = end {
&lines[context.original_index + 1..end]
} else {
context.following_lines
},
original_index: context.original_index,
is_last_section: end.is_none(),
});
end = Some(next_end);
}
truncated_contexts.reverse();
truncated_contexts
}

27
src/docstrings/styles.rs Normal file
View File

@@ -0,0 +1,27 @@
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
use crate::docstrings::google::{GOOGLE_SECTION_NAMES, LOWERCASE_GOOGLE_SECTION_NAMES};
use crate::docstrings::numpy::{LOWERCASE_NUMPY_SECTION_NAMES, NUMPY_SECTION_NAMES};
pub(crate) enum SectionStyle {
NumPy,
Google,
}
impl SectionStyle {
pub(crate) fn section_names(&self) -> &Lazy<BTreeSet<&'static str>> {
match self {
SectionStyle::NumPy => &NUMPY_SECTION_NAMES,
SectionStyle::Google => &GOOGLE_SECTION_NAMES,
}
}
pub(crate) fn lowercase_section_names(&self) -> &Lazy<BTreeSet<&'static str>> {
match self {
SectionStyle::NumPy => &LOWERCASE_NUMPY_SECTION_NAMES,
SectionStyle::Google => &LOWERCASE_GOOGLE_SECTION_NAMES,
}
}
}

View File

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

View File

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

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

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

View File

@@ -0,0 +1,12 @@
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 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 unary_prefix_increment;
mod unused_loop_control_variable;

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,387 @@
use num_bigint::BigInt;
use rustpython_ast::{Comprehension, Constant, Expr, ExprKind, 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],
) -> Option<&'a ExprKind> {
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]) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("list", func, args)?;
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]) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
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]) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
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],
) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
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],
) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
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]) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
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]) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
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::checks;
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) = checks::check_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)

View File

@@ -1,3 +1,5 @@
#![allow(clippy::collapsible_if, clippy::collapsible_else_if)]
use std::path::Path;
use anyhow::Result;
@@ -14,22 +16,34 @@ mod autofix;
pub mod cache;
pub mod check_ast;
mod check_lines;
mod check_tokens;
pub mod checks;
pub mod cli;
pub mod code_gen;
mod cst;
mod docstrings;
mod flake8_bugbear;
mod flake8_builtins;
mod flake8_comprehensions;
mod flake8_print;
pub mod fs;
pub mod linter;
pub mod logging;
pub mod message;
mod noqa;
mod plugins;
mod pep8_naming;
pub mod printer;
mod pycodestyle;
mod pydocstyle;
mod pyflakes;
pub mod pyproject;
mod python;
mod pyupgrade;
pub mod settings;
pub mod visibility;
/// Run ruff over Python source code directly.
pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
pub fn check(path: &Path, contents: &str, quiet: bool) -> Result<Vec<Message>> {
// Find the project root and pyproject.toml.
let project_root = pyproject::find_project_root(&[path.to_path_buf()]);
match &project_root {
@@ -42,7 +56,11 @@ pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings = Settings::from_raw(RawSettings::from_pyproject(&pyproject, &project_root)?);
let settings = Settings::from_raw(RawSettings::from_pyproject(
&pyproject,
&project_root,
quiet,
)?);
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
use std::io;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
@@ -9,9 +9,11 @@ use clap::Parser;
use colored::Colorize;
use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use walkdir::DirEntry;
#[cfg(not(target_family = "wasm"))]
use ruff::cache;
use ruff::checks::CheckCode;
use ruff::checks::CheckKind;
@@ -19,7 +21,7 @@ use ruff::cli::{warn_on, Cli, Warnable};
use ruff::fs::iter_python_files;
use ruff::linter::add_noqa_to_path;
use ruff::linter::autoformat_path;
use ruff::linter::lint_path;
use ruff::linter::{lint_path, lint_stdin};
use ruff::logging::set_up_logging;
use ruff::message::Message;
use ruff::printer::{Printer, SerializationFormat};
@@ -29,9 +31,24 @@ use ruff::settings::RawSettings;
use ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ruff::tell_user;
#[cfg(feature = "update-informer")]
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
#[cfg(feature = "update-informer")]
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Shim that calls par_iter except for wasm because there's no wasm support in rayon yet
/// (there is a shim to be used for the web, but it requires js cooperation)
/// Unfortunately, ParallelIterator does not implement Iterator so the signatures diverge
#[cfg(not(target_family = "wasm"))]
fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl ParallelIterator<Item = &T> {
iterable.par_iter()
}
#[cfg(target_family = "wasm")]
fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl Iterator<Item = &T> {
iterable.iter()
}
#[cfg(feature = "update-informer")]
fn check_for_updates() {
use update_informer::{registry, Check};
@@ -75,6 +92,19 @@ fn show_files(files: &[PathBuf], settings: &Settings) {
}
}
fn read_from_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
Ok(buffer)
}
fn run_once_stdin(settings: &Settings, filename: &Path, autofix: bool) -> Result<Vec<Message>> {
let stdin = read_from_stdin()?;
let mut messages = lint_stdin(filename, &stdin, settings, &autofix.into())?;
messages.sort_unstable();
Ok(messages)
}
fn run_once(
files: &[PathBuf],
settings: &Settings,
@@ -91,8 +121,7 @@ fn run_once(
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let mut messages: Vec<Message> = paths
.par_iter()
let mut messages: Vec<Message> = par_iter(&paths)
.map(|entry| {
match entry {
Ok(entry) => {
@@ -147,8 +176,7 @@ fn add_noqa(files: &[PathBuf], settings: &Settings) -> Result<usize> {
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications: usize = paths
.par_iter()
let modifications: usize = par_iter(&paths)
.map(|entry| match entry {
Ok(entry) => {
let path = entry.path();
@@ -177,8 +205,7 @@ fn autoformat(files: &[PathBuf], settings: &Settings) -> Result<usize> {
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications = paths
.par_iter()
let modifications = par_iter(&paths)
.map(|entry| {
let path = entry.path();
autoformat_path(path)
@@ -193,7 +220,8 @@ fn autoformat(files: &[PathBuf], settings: &Settings) -> Result<usize> {
}
fn inner_main() -> Result<ExitCode> {
let cli = Cli::parse();
let mut cli = Cli::parse();
cli.quiet |= cli.silent;
set_up_logging(cli.verbose)?;
@@ -203,7 +231,9 @@ fn inner_main() -> Result<ExitCode> {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
let pyproject = pyproject::find_pyproject_toml(&project_root);
let pyproject = cli
.config
.or_else(|| pyproject::find_pyproject_toml(&project_root));
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
@@ -226,7 +256,7 @@ fn inner_main() -> Result<ExitCode> {
.map(|pair| PerFileIgnore::new(pair, &project_root))
.collect();
let mut settings = RawSettings::from_pyproject(&pyproject, &project_root)?;
let mut settings = RawSettings::from_pyproject(&pyproject, &project_root, cli.quiet)?;
if !exclude.is_empty() {
settings.exclude = exclude;
}
@@ -287,6 +317,7 @@ fn inner_main() -> Result<ExitCode> {
return Ok(ExitCode::SUCCESS);
}
#[cfg(not(target_family = "wasm"))]
cache::init()?;
let mut printer = Printer::new(cli.format, cli.verbose);
@@ -312,7 +343,7 @@ fn inner_main() -> Result<ExitCode> {
tell_user!("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
if !cli.silent {
printer.write_continuously(&messages)?;
}
@@ -332,7 +363,7 @@ fn inner_main() -> Result<ExitCode> {
tell_user!("File change detected...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
if !cli.silent {
printer.write_continuously(&messages)?;
}
}
@@ -352,13 +383,30 @@ fn inner_main() -> Result<ExitCode> {
println!("Formatted {modifications} files.");
}
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
if !cli.quiet {
let (messages, should_print_messages, should_check_updates) =
if cli.files == vec![PathBuf::from("-")] {
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
let path = Path::new(&filename);
(
run_once_stdin(&settings, path, cli.fix)?,
!cli.silent && !cli.fix,
false,
)
} else {
(
run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?,
!cli.silent,
!cli.quiet,
)
};
if should_print_messages {
printer.write_once(&messages)?;
}
#[cfg(feature = "update-informer")]
check_for_updates();
if should_check_updates {
check_for_updates();
}
if messages.iter().any(|message| !message.fixed) && !cli.exit_zero {
return Ok(ExitCode::FAILURE);

View File

@@ -81,7 +81,7 @@ pub fn extract_noqa_line_for(lxr: &[LexResult]) -> Vec<usize> {
}
fn add_noqa_inner(
checks: &Vec<Check>,
checks: &[Check],
contents: &str,
noqa_line_for: &[usize],
) -> Result<(usize, String)> {
@@ -138,7 +138,7 @@ fn add_noqa_inner(
}
pub fn add_noqa(
checks: &Vec<Check>,
checks: &[Check],
contents: &str,
noqa_line_for: &[usize],
path: &Path,

261
src/pep8_naming/checks.rs Normal file
View File

@@ -0,0 +1,261 @@
use itertools::Itertools;
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use crate::ast::types::{Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option<Check> {
let stripped = name.strip_prefix('_').unwrap_or(name);
if !stripped
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
|| stripped.contains('_')
{
return Some(Check::new(
CheckKind::InvalidClassName(name.to_string()),
Range::from_located(class_def),
));
}
None
}
pub fn invalid_function_name(func_def: &Stmt, name: &str) -> Option<Check> {
if name.chars().any(|c| c.is_uppercase()) {
return Some(Check::new(
CheckKind::InvalidFunctionName(name.to_string()),
Range::from_located(func_def),
));
}
None
}
pub fn invalid_argument_name(location: Range, name: &str) -> Option<Check> {
if name.chars().any(|c| c.is_uppercase()) {
return Some(Check::new(
CheckKind::InvalidArgumentName(name.to_string()),
location,
));
}
None
}
pub fn invalid_first_argument_name_for_class_method(
scope: &Scope,
decorator_list: &[Expr],
args: &Arguments,
) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Class) {
return None;
}
if decorator_list.iter().any(|decorator| {
if let ExprKind::Name { id, .. } = &decorator.node {
id == "classmethod"
} else {
false
}
}) {
if let Some(arg) = args.args.first() {
if arg.node.arg != "cls" {
return Some(Check::new(
CheckKind::InvalidFirstArgumentNameForClassMethod,
Range::from_located(arg),
));
}
}
}
None
}
pub fn invalid_first_argument_name_for_method(
scope: &Scope,
decorator_list: &[Expr],
args: &Arguments,
) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Class) {
return None;
}
if decorator_list.iter().any(|decorator| {
if let ExprKind::Name { id, .. } = &decorator.node {
id == "classmethod" || id == "staticmethod"
} else {
false
}
}) {
return None;
}
if let Some(arg) = args.args.first() {
if arg.node.arg != "self" {
return Some(Check::new(
CheckKind::InvalidFirstArgumentNameForMethod,
Range::from_located(arg),
));
}
}
None
}
pub fn dunder_function_name(func_def: &Stmt, scope: &Scope, name: &str) -> Option<Check> {
if matches!(scope.kind, ScopeKind::Class) {
return None;
}
if name.starts_with("__") && name.ends_with("__") {
return Some(Check::new(
CheckKind::DunderFunctionName,
Range::from_located(func_def),
));
}
None
}
fn is_lower(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_uppercase() {
return false;
} else if !cased && c.is_lowercase() {
cased = true;
}
}
cased
}
fn is_upper(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_lowercase() {
return false;
} else if (!cased) && c.is_uppercase() {
cased = true;
}
}
cased
}
pub fn constant_imported_as_non_constant(
import_from: &Stmt,
name: &str,
asname: &str,
) -> Option<Check> {
if is_upper(name) && !is_upper(asname) {
return Some(Check::new(
CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
));
}
None
}
pub fn lowercase_imported_as_non_lowercase(
import_from: &Stmt,
name: &str,
asname: &str,
) -> Option<Check> {
if is_lower(name) && asname.to_lowercase() != asname {
return Some(Check::new(
CheckKind::LowercaseImportedAsNonLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
));
}
None
}
fn is_camelcase(name: &str) -> bool {
!is_lower(name) && !is_upper(name) && !name.contains('_')
}
fn is_acronym(name: &str, asname: &str) -> bool {
name.chars().filter(|c| c.is_uppercase()).join("") == asname
}
pub fn camelcase_imported_as_lowercase(
import_from: &Stmt,
name: &str,
asname: &str,
) -> Option<Check> {
if is_camelcase(name) && is_lower(asname) {
return Some(Check::new(
CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
));
}
None
}
pub fn camelcase_imported_as_constant(
import_from: &Stmt,
name: &str,
asname: &str,
) -> Option<Check> {
if is_camelcase(name) && is_upper(asname) && !is_acronym(name, asname) {
return Some(Check::new(
CheckKind::CamelcaseImportedAsConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
));
}
None
}
pub fn camelcase_imported_as_acronym(
import_from: &Stmt,
name: &str,
asname: &str,
) -> Option<Check> {
if is_camelcase(name) && is_upper(asname) && is_acronym(name, asname) {
return Some(Check::new(
CheckKind::CamelcaseImportedAsAcronym(name.to_string(), asname.to_string()),
Range::from_located(import_from),
));
}
None
}
#[cfg(test)]
mod tests {
use super::{is_acronym, is_camelcase, is_lower, is_upper};
#[test]
fn test_is_lower() -> () {
assert!(is_lower("abc"));
assert!(is_lower("a_b_c"));
assert!(is_lower("a2c"));
assert!(!is_lower("aBc"));
assert!(!is_lower("ABC"));
assert!(!is_lower(""));
assert!(!is_lower("_"));
}
#[test]
fn test_is_upper() -> () {
assert!(is_upper("ABC"));
assert!(is_upper("A_B_C"));
assert!(is_upper("A2C"));
assert!(!is_upper("aBc"));
assert!(!is_upper("abc"));
assert!(!is_upper(""));
assert!(!is_upper("_"));
}
#[test]
fn test_is_camelcase() -> () {
assert!(is_camelcase("Camel"));
assert!(is_camelcase("CamelCase"));
assert!(!is_camelcase("camel"));
assert!(!is_camelcase("camel_case"));
assert!(!is_camelcase("CAMEL"));
assert!(!is_camelcase("CAMEL_CASE"));
}
#[test]
fn test_is_acronym() -> () {
assert!(is_acronym("AB", "AB"));
assert!(is_acronym("AbcDef", "AD"));
assert!(!is_acronym("AbcDef", "Ad"));
assert!(!is_acronym("AbcDef", "AB"));
}
}

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