Compare commits

...

140 Commits

Author SHA1 Message Date
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
Charlie Marsh
10b250ee57 Bump version to 0.0.64 2022-10-09 17:38:09 -04:00
Charlie Marsh
30b1b1e15a Treat TypeAlias values as annotations (#377) 2022-10-09 17:37:19 -04:00
Charlie Marsh
aafe7c0c39 Mark aliased submodule imports as used (#374) 2022-10-09 17:01:14 -04:00
Harutaka Kawamura
f060248656 Fix collapsed message (#372) 2022-10-09 13:01:36 -04:00
Harutaka Kawamura
bbe0220c72 Implement C415 (#371) 2022-10-09 10:12:58 -04:00
Charlie Marsh
129e2b6ad3 Bump version to 0.0.63 2022-10-08 22:51:49 -04:00
Charlie Marsh
73e744b1d0 Create unified Expr for PEP 604 rewrites (#370) 2022-10-08 22:13:00 -04:00
Charlie Marsh
50a3fc5a67 Move some code into helpers.rs 2022-10-08 20:45:15 -04:00
Charlie Marsh
de499f0258 Optimize imports 2022-10-08 20:39:13 -04:00
Charlie Marsh
e1abe37c6a Bump version to 0.0.62 2022-10-08 20:28:38 -04:00
Charlie Marsh
7fe5945541 Implement PEP 604 annotation rewrites (#369) 2022-10-08 20:28:00 -04:00
Charlie Marsh
806f3fd4f6 Implement PEP 585 annotation rewrites (#368) 2022-10-08 18:22:24 -04:00
Charlie Marsh
2bba643dd2 Use strum to facilitate simple enum serialization (#367) 2022-10-08 17:47:25 -04:00
Charlie Marsh
54090bd7ac Use strum to iterate over all check codes (#366) 2022-10-08 17:41:47 -04:00
Charlie Marsh
c62727db42 Bump version to 0.0.61 2022-10-08 17:25:36 -04:00
Charlie Marsh
d0e1612507 Check newline ending on contents directly (#365) 2022-10-08 17:25:22 -04:00
Harutaka Kawamura
5ccd907398 Implement C408 (#364) 2022-10-08 17:17:34 -04:00
Harutaka Kawamura
346610c2e3 Implement C406 (#363) 2022-10-08 11:15:41 -04:00
Harutaka Kawamura
307fa26515 Implement C405 (#362) 2022-10-08 09:03:21 -04:00
Harutaka Kawamura
136d412edd Add missing C400,C401, and C402 to CheckCode.from_str (#361) 2022-10-08 09:02:03 -04:00
Harutaka Kawamura
d9edec0ac9 Implement C402 (#359) 2022-10-07 22:57:04 -04:00
Chris Pryer
473675fffb Add check for W292 (#339) 2022-10-07 21:10:16 -04:00
Steven Maude
95dfc61315 Update GitHub Actions versions in README (#358) 2022-10-07 20:24:00 -04:00
Charlie Marsh
dd496c7b52 Bump version to 0.0.60 2022-10-07 17:36:33 -04:00
Charlie Marsh
78aafb4b34 Warn the user if an explicitly selected check code is ignored (#356) 2022-10-07 17:36:17 -04:00
Charlie Marsh
99c66d513a Rename refactor checks to upgrade checks (#354) 2022-10-07 16:54:55 -04:00
Charlie Marsh
04ade6a2f3 Update README.md 2022-10-07 16:50:17 -04:00
Charlie Marsh
4cf2682cda Implement type(primitive) (#353) 2022-10-07 16:47:06 -04:00
Charlie Marsh
e3a7357187 Wrap each import in its own backticks (#346) 2022-10-07 15:58:30 -04:00
Charlie Marsh
b60768debb Remove erroneous output.py file 2022-10-07 15:04:23 -04:00
Charlie Marsh
25e476639f Use or_default in lieu of or_insert 2022-10-07 14:56:56 -04:00
Charlie Marsh
4645788205 Bump version to 0.0.59 2022-10-07 14:55:23 -04:00
Charlie Marsh
0b9eda8836 Add target Python version as a configurable setting (#344) 2022-10-07 14:54:50 -04:00
Charlie Marsh
ad23d6acee Remove :: prefix for ruff imports 2022-10-07 14:24:51 -04:00
Harutaka Kawamura
e3d1d01a1f Implement C401 (#343) 2022-10-07 13:00:22 -04:00
Harutaka Kawamura
6dfdd21a7c Implement C400 (#340) 2022-10-07 12:16:23 -04:00
265 changed files with 13263 additions and 3718 deletions

307
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.58"
version = "0.0.81"
dependencies = [
"anyhow",
"assert_cmd",
"bincode",
"cacache",
"chrono",
@@ -1920,12 +2059,14 @@ dependencies = [
"dirs 4.0.0",
"fern",
"filetime",
"getrandom 0.2.7",
"glob",
"insta",
"itertools",
"libcst",
"log",
"notify",
"num-bigint",
"once_cell",
"path-absolutize",
"rayon",
@@ -1935,6 +2076,11 @@ dependencies = [
"rustpython-parser",
"serde",
"serde_json",
"strum",
"strum_macros",
"test-case",
"textwrap",
"titlecase",
"toml",
"update-informer",
"walkdir",
@@ -1955,7 +2101,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -1965,7 +2111,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
dependencies = [
"ascii",
"cfg-if 1.0.0",
@@ -1988,7 +2134,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
dependencies = [
"bincode",
"bitflags",
@@ -2005,7 +2151,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
dependencies = [
"ahash",
"anyhow",
@@ -2053,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"
@@ -2091,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",
@@ -2196,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"
@@ -2253,10 +2411,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.101"
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
dependencies = [
"proc-macro2",
"quote",
@@ -2320,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"
@@ -2375,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"
@@ -2472,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"
@@ -2485,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"
@@ -2568,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.58"
version = "0.0.81"
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,21 +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 = "4f457893efc381ad5c432576b24bcc7e4a08c641" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "4f457893efc381ad5c432576b24bcc7e4a08c641" }
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "4f457893efc381ad5c432576b24bcc7e4a08c641" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/charliermarsh/RustPython.git", rev = "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" }
[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"]

521
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.81
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]
@@ -94,6 +114,8 @@ Arguments:
<FILES>...
Options:
--config <CONFIG>
Path to the `pyproject.toml` file to use for configuration
-v, --verbose
Enable verbose logging
-q, --quiet
@@ -130,6 +152,10 @@ Options:
Enable automatic additions of noqa directives to failing lines
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
-h, --help
Print help information
-V, --version
@@ -152,7 +178,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
@@ -176,133 +202,220 @@ 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: `...` | ✅ | |
| 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 | | |
| C403 | UnnecessaryListComprehensionSet | Unnecessary list comprehension - rewrite as a set comprehension | | |
| C404 | UnnecessaryListComprehensionDict | Unnecessary list comprehension - rewrite as a dict comprehension | | |
| 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 | | 🛠 |
| R001 | UselessObjectInheritance | Class `...` inherits from object | | 🛠 |
| R002 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 |
| 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
| 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: `...` | |
| W292 | NoNewLineAtEndOfFile | No newline at end of file | |
### 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` | |
### 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 |
| ---- | ---- | ------- | --- |
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 |
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
### 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
@@ -311,22 +424,184 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install dependencies
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/) (3/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/) (3/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/2830) 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:
@@ -338,13 +613,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.
@@ -421,9 +696,9 @@ hyperfine --ignore-failure --warmup 5 \
In order, these evaluate:
- ruff
- Ruff
- Pylint
- PyFlakes
- Pyflakes
- autoflake
- pycodestyle
- Flake8

View File

@@ -1,27 +1,29 @@
/// Generate a Markdown-compatible table of supported lint rules.
use ruff::checks::{CheckCode, ALL_CHECK_CODES, DEFAULT_CHECK_CODES};
//! Generate a Markdown-compatible table of supported lint rules.
use strum::IntoEnumIterator;
use ruff::checks::{CheckCategory, CheckCode};
fn main() {
let mut check_codes: Vec<CheckCode> = ALL_CHECK_CODES.to_vec();
check_codes.sort();
for check_category in CheckCategory::iter() {
println!("### {}", check_category.title());
println!();
println!("| Code | Name | Message | | |");
println!("| ---- | ---- | ------- | --- | --- |");
for check_code in check_codes {
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_str(),
check_kind.name(),
check_kind.body(),
default_token,
fix_token
);
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.body().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;

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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,28 @@ 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)
# Test: access a sub-importation via an alias.
import pyarrow as pa
import pyarrow.csv
print(pa.csv.read_csv("test.csv"))
# Test: referencing an import via TypeAlias.
import sys
import numpy as np
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
CustomInt: TypeAlias = "np.int8 | np.int16"

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

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

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

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

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

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

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

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

View File

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

View File

@@ -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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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=[])

View File

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

View File

@@ -1,861 +0,0 @@
use std::collections::BTreeSet;
use itertools::izip;
use regex::Regex;
use rustpython_parser::ast::{
Arg, ArgData, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind,
Stmt, StmtKind, Unaryop,
};
use crate::ast::types::{
Binding, BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind,
};
use crate::checks::{Check, CheckKind, RejectedCmpop};
use crate::python::builtins::BUILTINS;
/// Check IfTuple compliance.
pub fn check_if_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
return Some(Check::new(CheckKind::IfTuple, location));
}
}
None
}
/// Check AssertTuple compliance.
pub fn check_assert_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
return Some(Check::new(CheckKind::AssertTuple, location));
}
}
None
}
/// Check NotInTest and NotIsTest compliance.
pub fn check_not_tests(
op: &Unaryop,
operand: &Expr,
check_not_in: bool,
check_not_is: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
if matches!(op, Unaryop::Not) {
if let ExprKind::Compare { ops, .. } = &operand.node {
for op in ops {
match op {
Cmpop::In => {
if check_not_in {
checks.push(Check::new(
CheckKind::NotInTest,
locator.locate_check(Range::from_located(operand)),
));
}
}
Cmpop::Is => {
if check_not_is {
checks.push(Check::new(
CheckKind::NotIsTest,
locator.locate_check(Range::from_located(operand)),
));
}
}
_ => {}
}
}
}
}
checks
}
/// Check UnusedVariable compliance.
pub fn check_unused_variables(
scope: &Scope,
locator: &dyn CheckLocator,
dummy_variable_rgx: &Regex,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
if matches!(
scope.kind,
ScopeKind::Function(FunctionScope { uses_locals: true })
) {
return checks;
}
for (name, binding) in scope.values.iter() {
if binding.used.is_none()
&& matches!(binding.kind, BindingKind::Assignment)
&& !dummy_variable_rgx.is_match(name)
&& name != "__tracebackhide__"
&& name != "__traceback_info__"
&& name != "__traceback_supplement__"
{
checks.push(Check::new(
CheckKind::UnusedVariable(name.to_string()),
locator.locate_check(binding.range),
));
}
}
checks
}
/// Check DoNotAssignLambda compliance.
pub fn check_do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Lambda { .. } = &value.node {
Some(Check::new(CheckKind::DoNotAssignLambda, location))
} else {
None
}
}
/// Check UselessMetaclassType compliance.
pub fn check_useless_metaclass_type(
targets: &Vec<Expr>,
value: &Expr,
location: Range,
) -> Option<Check> {
if targets.len() == 1 {
if let ExprKind::Name { id, .. } = targets.first().map(|expr| &expr.node).unwrap() {
if id == "__metaclass__" {
if let ExprKind::Name { id, .. } = &value.node {
if id == "type" {
return Some(Check::new(CheckKind::UselessMetaclassType, location));
}
}
}
}
}
None
}
/// Check UnnecessaryAbspath compliance.
pub fn check_unnecessary_abspath(func: &Expr, args: &Vec<Expr>, location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &args[0].node {
if id == "__file__" {
match &func.node {
ExprKind::Attribute { attr: id, .. } | ExprKind::Name { id, .. } => {
if id == "abspath" {
return Some(Check::new(CheckKind::UnnecessaryAbspath, location));
}
}
_ => {}
}
}
}
}
None
}
fn is_ambiguous_name(name: &str) -> bool {
name == "l" || name == "I" || name == "O"
}
/// Check AmbiguousVariableName compliance.
pub fn check_ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousVariableName(name.to_string()),
location,
))
} else {
None
}
}
/// Check AmbiguousClassName compliance.
pub fn check_ambiguous_class_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousClassName(name.to_string()),
location,
))
} else {
None
}
}
/// Check AmbiguousFunctionName compliance.
pub fn check_ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousFunctionName(name.to_string()),
location,
))
} else {
None
}
}
/// Check UselessObjectInheritance compliance.
pub fn check_useless_object_inheritance(
name: &str,
bases: &[Expr],
scope: &Scope,
) -> Option<Check> {
for expr in bases {
if let ExprKind::Name { id, .. } = &expr.node {
if id == "object" {
match scope.values.get(id) {
None
| Some(Binding {
kind: BindingKind::Builtin,
..
}) => {
return Some(Check::new(
CheckKind::UselessObjectInheritance(name.to_string()),
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
None
}
/// Check DefaultExceptNotLast compliance.
pub fn check_default_except_not_last(handlers: &Vec<Excepthandler>) -> Option<Check> {
for (idx, handler) in handlers.iter().enumerate() {
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
if type_.is_none() && idx < handlers.len() - 1 {
return Some(Check::new(
CheckKind::DefaultExceptNotLast,
Range::from_located(handler),
));
}
}
None
}
/// Check RaiseNotImplemented compliance.
pub fn check_raise_not_implemented(expr: &Expr) -> Option<Check> {
match &expr.node {
ExprKind::Call { func, .. } => {
if let ExprKind::Name { id, .. } = &func.node {
if id == "NotImplemented" {
return Some(Check::new(
CheckKind::RaiseNotImplemented,
Range::from_located(expr),
));
}
}
}
ExprKind::Name { id, .. } => {
if id == "NotImplemented" {
return Some(Check::new(
CheckKind::RaiseNotImplemented,
Range::from_located(expr),
));
}
}
_ => {}
}
None
}
/// Check DuplicateArgumentName compliance.
pub fn check_duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
// Collect all the arguments into a single vector.
let mut all_arguments: Vec<&Arg> = arguments
.args
.iter()
.chain(arguments.posonlyargs.iter())
.chain(arguments.kwonlyargs.iter())
.collect();
if let Some(arg) = &arguments.vararg {
all_arguments.push(arg);
}
if let Some(arg) = &arguments.kwarg {
all_arguments.push(arg);
}
// Search for duplicates.
let mut idents: BTreeSet<&str> = BTreeSet::new();
for arg in all_arguments {
let ident = &arg.node.arg;
if idents.contains(ident.as_str()) {
checks.push(Check::new(
CheckKind::DuplicateArgumentName,
Range::from_located(arg),
));
}
idents.insert(ident);
}
checks
}
/// Check AssertEquals compliance.
pub fn check_assert_equals(expr: &Expr) -> Option<Check> {
if let ExprKind::Attribute { value, attr, .. } = &expr.node {
if attr == "assertEquals" {
if let ExprKind::Name { id, .. } = &value.node {
if id == "self" {
return Some(Check::new(
CheckKind::NoAssertEquals,
Range::from_located(expr),
));
}
}
}
}
None
}
#[derive(Debug, PartialEq)]
enum DictionaryKey<'a> {
Constant(&'a Constant),
Variable(&'a String),
}
fn convert_to_value(expr: &Expr) -> Option<DictionaryKey> {
match &expr.node {
ExprKind::Constant { value, .. } => Some(DictionaryKey::Constant(value)),
ExprKind::Name { id, .. } => Some(DictionaryKey::Variable(id)),
_ => None,
}
}
/// Check MultiValueRepeatedKeyLiteral and MultiValueRepeatedKeyVariable compliance.
pub fn check_repeated_keys(
keys: &Vec<Expr>,
check_repeated_literals: bool,
check_repeated_variables: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
let num_keys = keys.len();
for i in 0..num_keys {
let k1 = &keys[i];
let v1 = convert_to_value(k1);
for k2 in keys.iter().take(num_keys).skip(i + 1) {
let v2 = convert_to_value(k2);
match (&v1, &v2) {
(Some(DictionaryKey::Constant(v1)), Some(DictionaryKey::Constant(v2))) => {
if check_repeated_literals && v1 == v2 {
checks.push(Check::new(
CheckKind::MultiValueRepeatedKeyLiteral,
locator.locate_check(Range::from_located(k2)),
))
}
}
(Some(DictionaryKey::Variable(v1)), Some(DictionaryKey::Variable(v2))) => {
if check_repeated_variables && v1 == v2 {
checks.push(Check::new(
CheckKind::MultiValueRepeatedKeyVariable((*v2).to_string()),
locator.locate_check(Range::from_located(k2)),
))
}
}
_ => {}
}
}
}
checks
}
/// Check TrueFalseComparison and NoneComparison compliance.
pub fn check_literal_comparisons(
left: &Expr,
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
check_none_comparisons: bool,
check_true_false_comparisons: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
let op = ops.first().unwrap();
let comparator = left;
// Check `left`.
if check_none_comparisons
&& matches!(
comparator.node,
ExprKind::Constant {
value: Constant::None,
kind: None
}
)
{
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::Eq),
locator.locate_check(Range::from_located(comparator)),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
locator.locate_check(Range::from_located(comparator)),
));
}
}
if check_true_false_comparisons {
if let ExprKind::Constant {
value: Constant::Bool(value),
kind: None,
} = comparator.node
{
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
locator.locate_check(Range::from_located(comparator)),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
locator.locate_check(Range::from_located(comparator)),
));
}
}
}
// Check each comparator in order.
for (op, comparator) in izip!(ops, comparators) {
if check_none_comparisons
&& matches!(
comparator.node,
ExprKind::Constant {
value: Constant::None,
kind: None
}
)
{
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::Eq),
locator.locate_check(Range::from_located(comparator)),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
locator.locate_check(Range::from_located(comparator)),
));
}
}
if check_true_false_comparisons {
if let ExprKind::Constant {
value: Constant::Bool(value),
kind: None,
} = comparator.node
{
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
locator.locate_check(Range::from_located(comparator)),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
locator.locate_check(Range::from_located(comparator)),
));
}
}
}
}
checks
}
fn is_constant(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Constant { .. } => true,
ExprKind::Tuple { elts, .. } => elts.iter().all(is_constant),
_ => false,
}
}
fn is_singleton(expr: &Expr) -> bool {
matches!(
expr.node,
ExprKind::Constant {
value: Constant::None | Constant::Bool(_) | Constant::Ellipsis,
..
}
)
}
fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// Check IsLiteral compliance.
pub fn check_is_literal(
left: &Expr,
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
location: Range,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
let mut left = left;
for (op, right) in izip!(ops, comparators) {
if matches!(op, Cmpop::Is | Cmpop::IsNot)
&& (is_constant_non_singleton(left) || is_constant_non_singleton(right))
{
checks.push(Check::new(CheckKind::IsLiteral, location));
}
left = right;
}
checks
}
/// Check TypeComparison compliance.
pub fn check_type_comparison(
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
location: Range,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for (op, right) in izip!(ops, comparators) {
if matches!(op, Cmpop::Is | Cmpop::IsNot | Cmpop::Eq | Cmpop::NotEq) {
match &right.node {
ExprKind::Call { func, args, .. } => {
if let ExprKind::Name { id, .. } = &func.node {
// Ex) type(False)
if id == "type" {
if let Some(arg) = args.first() {
// Allow comparison for types which are not obvious.
if !matches!(arg.node, ExprKind::Name { .. }) {
checks.push(Check::new(CheckKind::TypeComparison, location));
}
}
}
}
}
ExprKind::Attribute { value, .. } => {
if let ExprKind::Name { id, .. } = &value.node {
// Ex) types.IntType
if id == "types" {
checks.push(Check::new(CheckKind::TypeComparison, location));
}
}
}
_ => {}
}
}
}
checks
}
/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance.
pub fn check_starred_expressions(
elts: &[Expr],
check_too_many_expressions: bool,
check_two_starred_expressions: bool,
location: Range,
) -> Option<Check> {
let mut has_starred: bool = false;
let mut starred_index: Option<usize> = None;
for (index, elt) in elts.iter().enumerate() {
if matches!(elt.node, ExprKind::Starred { .. }) {
if has_starred && check_two_starred_expressions {
return Some(Check::new(CheckKind::TwoStarredExpressions, location));
}
has_starred = true;
starred_index = Some(index);
}
}
if check_too_many_expressions {
if let Some(starred_index) = starred_index {
if starred_index >= 1 << 8 || elts.len() - starred_index > 1 << 24 {
return Some(Check::new(CheckKind::ExpressionsInStarAssignment, location));
}
}
}
None
}
/// Check BreakOutsideLoop compliance.
pub fn check_break_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
locator: &dyn CheckLocator,
) -> Option<Check> {
let mut allowed: bool = false;
let mut parent = stmt;
for index in parent_stack.iter().rev() {
let child = parent;
parent = parents[*index];
match &parent.node {
StmtKind::For { orelse, .. }
| StmtKind::AsyncFor { orelse, .. }
| StmtKind::While { orelse, .. } => {
if !orelse.contains(child) {
allowed = true;
break;
}
}
StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
| StmtKind::ClassDef { .. } => {
break;
}
_ => {}
}
}
if !allowed {
Some(Check::new(
CheckKind::BreakOutsideLoop,
locator.locate_check(Range::from_located(stmt)),
))
} else {
None
}
}
/// Check ContinueOutsideLoop compliance.
pub fn check_continue_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
locator: &dyn CheckLocator,
) -> Option<Check> {
let mut allowed: bool = false;
let mut parent = stmt;
for index in parent_stack.iter().rev() {
let child = parent;
parent = parents[*index];
match &parent.node {
StmtKind::For { orelse, .. }
| StmtKind::AsyncFor { orelse, .. }
| StmtKind::While { orelse, .. } => {
if !orelse.contains(child) {
allowed = true;
break;
}
}
StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
| StmtKind::ClassDef { .. } => {
break;
}
_ => {}
}
}
if !allowed {
Some(Check::new(
CheckKind::ContinueOutsideLoop,
locator.locate_check(Range::from_located(stmt)),
))
} else {
None
}
}
// flake8-builtins
pub enum ShadowingType {
Variable,
Argument,
Attribute,
}
/// Check builtin name shadowing
pub fn check_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
}
}
/// Returns `true` if a call is an argumented `super` invocation.
pub fn is_super_call_with_arguments(func: &Expr, args: &Vec<Expr>) -> bool {
// Check: is this a `super` call?
if let ExprKind::Name { id, .. } = &func.node {
id == "super" && !args.is_empty()
} else {
false
}
}
// flakes8-comprehensions
/// Check `set([...])` compliance.
pub fn unnecessary_list_comprehension_set(
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" {
if let ExprKind::ListComp { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionSet,
Range::from_located(expr),
));
}
}
}
}
None
}
/// Check `dict([...])` compliance.
pub fn unnecessary_list_comprehension_dict(
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "dict" {
if let ExprKind::ListComp { elt, .. } = &args[0].node {
match &elt.node {
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionDict,
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
}
None
}
// flake8-super
/// Check that `super()` has no args
pub fn check_super_args(
scope: &Scope,
parents: &[&Stmt],
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) -> Option<Check> {
if !is_super_call_with_arguments(func, args) {
return None;
}
// Check: are we in a Function scope?
if !matches!(scope.kind, ScopeKind::Function { .. }) {
return None;
}
let mut parents = parents.iter().rev();
// For a `super` invocation to be unnecessary, the first argument needs to match the enclosing
// class, and the second argument needs to match the first argument to the enclosing function.
if let [first_arg, second_arg] = args.as_slice() {
// Find the enclosing function definition (if any).
if let Some(StmtKind::FunctionDef {
args: parent_args, ..
}) = parents
.find(|stmt| matches!(stmt.node, StmtKind::FunctionDef { .. }))
.map(|stmt| &stmt.node)
{
// Extract the name of the first argument to the enclosing function.
if let Some(ArgData {
arg: parent_arg, ..
}) = parent_args.args.first().map(|expr| &expr.node)
{
// Find the enclosing class definition (if any).
if let Some(StmtKind::ClassDef {
name: parent_name, ..
}) = parents
.find(|stmt| matches!(stmt.node, StmtKind::ClassDef { .. }))
.map(|stmt| &stmt.node)
{
if let (
ExprKind::Name {
id: first_arg_id, ..
},
ExprKind::Name {
id: second_arg_id, ..
},
) = (&first_arg.node, &second_arg.node)
{
if first_arg_id == parent_name && second_arg_id == parent_arg {
return Some(Check::new(
CheckKind::SuperCallWithParameters,
Range::from_located(expr),
));
}
}
}
}
}
}
None
}
// flake8-print
/// Check whether a function call is a `print` or `pprint` invocation
pub fn check_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
}

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

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

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

View File

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

View File

@@ -18,7 +18,9 @@ impl Range {
pub fn from_located<T>(located: &Located<T>) -> Self {
Range {
location: located.location,
end_location: located.end_location,
end_location: located
.end_location
.expect("AST nodes should have end_location."),
}
}
}
@@ -74,9 +76,9 @@ pub enum BindingKind {
Export(Vec<String>),
FutureImportation,
StarImportation,
Importation(String, BindingContext),
FromImportation(String, BindingContext),
SubmoduleImportation(String, BindingContext),
Importation(String, String, BindingContext),
FromImportation(String, String, BindingContext),
SubmoduleImportation(String, String, BindingContext),
}
#[derive(Clone, Debug)]

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
use std::fs;
use std::path::Path;
use anyhow::Result;
use crate::autofix::Fix;
use crate::autofix::Patch;
use itertools::Itertools;
use rustpython_parser::ast::Location;
use std::collections::BTreeSet;
use crate::checks::{Check, Fix};
use crate::checks::Check;
#[derive(Hash)]
pub enum Mode {
@@ -24,51 +23,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 +102,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 +119,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 +145,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 +172,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 +208,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,11 @@
use std::collections::BTreeMap;
use crate::ast::types::Range;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::autofix::Fix;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::noqa;
use crate::noqa::Directive;
use crate::settings::Settings;
@@ -66,12 +67,12 @@ pub fn check_lines(
match noqa {
(Directive::All(_, _), matches) => {
matches.push(check.kind.code().as_str());
matches.push(check.kind.code().as_ref());
ignored.push(index)
}
(Directive::Codes(_, _, codes), matches) => {
if codes.contains(&check.kind.code().as_str()) {
matches.push(check.kind.code().as_str());
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
ignored.push(index);
}
}
@@ -98,11 +99,11 @@ pub fn check_lines(
match noqa {
(Directive::All(_, _), matches) => {
matches.push(check.kind.code().as_str());
matches.push(check.kind.code().as_ref());
}
(Directive::Codes(_, _, codes), matches) => {
if codes.contains(&check.kind.code().as_str()) {
matches.push(check.kind.code().as_str());
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
} else {
line_checks.push(check);
}
@@ -113,6 +114,45 @@ pub fn check_lines(
}
}
// Enforce newlines at end of files.
if settings.enabled.contains(&CheckCode::W292) && !contents.ends_with('\n') {
// Note: if `lines.last()` is `None`, then `contents` is empty (and so we don't want to
// raise W292 anyway).
if let Some(line) = lines.last() {
let lineno = lines.len() - 1;
let noqa_lineno = noqa_line_for
.get(lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
let check = Check::new(
CheckKind::NoNewLineAtEndOfFile,
Range {
location: Location::new(lines.len(), line.len() + 1),
end_location: Location::new(lines.len(), line.len() + 1),
},
);
match noqa {
(Directive::All(_, _), matches) => {
matches.push(check.kind.code().as_ref());
}
(Directive::Codes(_, _, codes), matches) => {
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
} else {
line_checks.push(check);
}
}
(Directive::None, _) => line_checks.push(check),
}
}
}
// Enforce that the noqa directive was actually used.
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
@@ -127,15 +167,10 @@ pub fn check_lines(
},
);
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,
});
check.amend(Fix::deletion(
Location::new(row + 1, start + 1),
Location::new(row + 1, lines[row].chars().count() + 1),
));
}
line_checks.push(check);
}
@@ -145,15 +180,15 @@ pub fn check_lines(
let mut valid_codes = vec![];
for code in codes {
if !matches.contains(&code) {
invalid_codes.push(code);
invalid_codes.push(code.to_string());
} else {
valid_codes.push(code);
valid_codes.push(code.to_string());
}
}
if !invalid_codes.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(invalid_codes.join(", "))),
CheckKind::UnusedNOQA(Some(invalid_codes)),
Range {
location: Location::new(row + 1, start + 1),
end_location: Location::new(row + 1, end + 1),
@@ -161,25 +196,16 @@ pub fn check_lines(
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
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);

File diff suppressed because it is too large Load Diff

142
src/cli.rs Normal file
View File

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

View File

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

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

View File

@@ -0,0 +1,114 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Stmt};
use crate::ast::helpers;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::fixer;
use crate::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 matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
// TODO(charlie): If we have a single element, remove the tuple.
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_expr(&type_pattern(unique_elts), 0) {
if let Ok(content) = generator.generate() {
check.amend(Fix::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,6 @@
pub use assert_false::assert_false;
pub use duplicate_exceptions::duplicate_exceptions;
pub use duplicate_exceptions::duplicate_handler_exceptions;
mod assert_false;
mod duplicate_exceptions;

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,505 @@
use num_bigint::BigInt;
use rustpython_ast::{Comprehension, Constant, Expr, ExprKind, KeywordData, Located, Unaryop};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
/// Check `list(generator)` compliance.
pub fn unnecessary_generator_list(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "list" {
if let ExprKind::GeneratorExp { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryGeneratorList,
Range::from_located(expr),
));
}
}
}
}
None
}
/// Check `set(generator)` compliance.
pub fn unnecessary_generator_set(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" {
if let ExprKind::GeneratorExp { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryGeneratorSet,
Range::from_located(expr),
));
}
}
}
}
None
}
/// Check `dict((x, y) for x, y in iterable)` compliance.
pub fn unnecessary_generator_dict(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "dict" {
if let ExprKind::GeneratorExp { elt, .. } = &args[0].node {
match &elt.node {
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryGeneratorDict,
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
}
None
}
/// Check `set([...])` compliance.
pub fn unnecessary_list_comprehension_set(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" {
if let ExprKind::ListComp { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionSet,
Range::from_located(expr),
));
}
}
}
}
None
}
/// Check `dict([...])` compliance.
pub fn unnecessary_list_comprehension_dict(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "dict" {
if let ExprKind::ListComp { elt, .. } = &args[0].node {
match &elt.node {
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryListComprehensionDict,
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
}
None
}
/// Check `set([1, 2])` compliance.
pub fn unnecessary_literal_set(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" {
match &args[0].node {
ExprKind::List { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralSet("list".to_string()),
Range::from_located(expr),
));
}
ExprKind::Tuple { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralSet("tuple".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
None
}
/// Check `dict([(1, 2)])` compliance.
pub fn unnecessary_literal_dict(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &func.node {
if id == "dict" {
match &args[0].node {
ExprKind::Tuple { elts, .. } => {
if let Some(elt) = elts.first() {
match &elt.node {
// dict((1, 2), ...))
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralDict("tuple".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
} else {
// dict(())
return Some(Check::new(
CheckKind::UnnecessaryLiteralDict("tuple".to_string()),
Range::from_located(expr),
));
}
}
ExprKind::List { elts, .. } => {
if let Some(elt) = elts.first() {
match &elt.node {
// dict([(1, 2), ...])
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralDict("list".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
} else {
// dict([])
return Some(Check::new(
CheckKind::UnnecessaryLiteralDict("list".to_string()),
Range::from_located(expr),
));
}
}
_ => {}
}
}
}
}
None
}
pub fn unnecessary_collection_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Located<KeywordData>],
) -> Option<Check> {
if args.is_empty() {
if let ExprKind::Name { id, .. } = &func.node {
if id == "list" || id == "tuple" {
// list() or tuple()
return Some(Check::new(
CheckKind::UnnecessaryCollectionCall(id.to_string()),
Range::from_located(expr),
));
} else if id == "dict" {
// dict() or dict(a=1)
if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) {
return Some(Check::new(
CheckKind::UnnecessaryCollectionCall(id.to_string()),
Range::from_located(expr),
));
}
}
}
}
None
}
pub fn unnecessary_literal_within_tuple_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "tuple" {
if let Some(arg) = args.first() {
match &arg.node {
ExprKind::Tuple { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinTupleCall("tuple".to_string()),
Range::from_located(expr),
));
}
ExprKind::List { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinTupleCall("list".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
None
}
pub fn unnecessary_literal_within_list_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "list" {
if let Some(arg) = args.first() {
match &arg.node {
ExprKind::Tuple { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinListCall("tuple".to_string()),
Range::from_located(expr),
));
}
ExprKind::List { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinListCall("list".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
None
}
pub fn unnecessary_list_call(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "list" {
if let Some(arg) = args.first() {
if let ExprKind::ListComp { .. } = &arg.node {
return Some(Check::new(
CheckKind::UnnecessaryListCall,
Range::from_located(expr),
));
}
}
}
}
None
}
pub fn unnecessary_call_around_sorted(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if let ExprKind::Name { id: outer, .. } = &func.node {
if outer == "list" || outer == "reversed" {
if let Some(arg) = args.first() {
if let ExprKind::Call { func, .. } = &arg.node {
if let ExprKind::Name { id: inner, .. } = &func.node {
if inner == "sorted" {
return Some(Check::new(
CheckKind::UnnecessaryCallAroundSorted(outer.to_string()),
Range::from_located(expr),
));
}
}
}
}
}
}
None
}
pub fn unnecessary_double_cast_or_process(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if let ExprKind::Name { id: outer, .. } = &func.node {
if outer == "list"
|| outer == "tuple"
|| outer == "set"
|| outer == "reversed"
|| outer == "sorted"
{
if let Some(arg) = args.first() {
if let ExprKind::Call { func, .. } = &arg.node {
if let ExprKind::Name { id: inner, .. } = &func.node {
// Ex) set(tuple(...))
if (outer == "set" || outer == "sorted")
&& (inner == "list"
|| inner == "tuple"
|| inner == "reversed"
|| inner == "sorted")
{
return Some(Check::new(
CheckKind::UnnecessaryDoubleCastOrProcess(
inner.to_string(),
outer.to_string(),
),
Range::from_located(expr),
));
}
// Ex) list(tuple(...))
if (outer == "list" || outer == "tuple")
&& (inner == "list" || inner == "tuple")
{
return Some(Check::new(
CheckKind::UnnecessaryDoubleCastOrProcess(
inner.to_string(),
outer.to_string(),
),
Range::from_located(expr),
));
}
// Ex) set(set(...))
if outer == "set" && inner == "set" {
return Some(Check::new(
CheckKind::UnnecessaryDoubleCastOrProcess(
inner.to_string(),
outer.to_string(),
),
Range::from_located(expr),
));
}
}
}
}
}
}
None
}
pub fn unnecessary_subscript_reversal(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if let Some(first_arg) = args.first() {
if let ExprKind::Name { id, .. } = &func.node {
if id == "set" || id == "sorted" || id == "reversed" {
if let ExprKind::Subscript { slice, .. } = &first_arg.node {
if let ExprKind::Slice { lower, upper, step } = &slice.node {
if lower.is_none() && upper.is_none() {
if let Some(step) = step {
if let ExprKind::UnaryOp {
op: Unaryop::USub,
operand,
} = &step.node
{
if let ExprKind::Constant {
value: Constant::Int(val),
..
} = &operand.node
{
if *val == BigInt::from(1) {
return Some(Check::new(
CheckKind::UnnecessarySubscriptReversal(
id.to_string(),
),
Range::from_located(expr),
));
}
}
}
}
}
}
}
}
}
}
None
}
pub fn unnecessary_comprehension(
expr: &Expr,
elt: &Expr,
generators: &[Comprehension],
) -> Option<Check> {
if generators.len() == 1 {
let generator = &generators[0];
if generator.ifs.is_empty() && generator.is_async == 0 {
if let ExprKind::Name { id: elt_id, .. } = &elt.node {
if let ExprKind::Name { id: target_id, .. } = &generator.target.node {
if elt_id == target_id {
match &expr.node {
ExprKind::ListComp { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryComprehension("list".to_string()),
Range::from_located(expr),
))
}
ExprKind::SetComp { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryComprehension("set".to_string()),
Range::from_located(expr),
))
}
_ => {}
};
}
}
}
}
}
None
}
pub fn unnecessary_map(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "map" {
if args.len() == 2 {
if let ExprKind::Lambda { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryMap("generator".to_string()),
Range::from_located(expr),
));
}
}
} else if id == "list" || id == "set" {
if let Some(arg) = args.first() {
if let ExprKind::Call { func, args, .. } = &arg.node {
if let ExprKind::Name { id: f, .. } = &func.node {
if f == "map" {
if let Some(arg) = args.first() {
if let ExprKind::Lambda { .. } = &arg.node {
return Some(Check::new(
CheckKind::UnnecessaryMap(id.to_string()),
Range::from_located(expr),
));
}
}
}
}
}
}
} else if id == "dict" {
if args.len() == 1 {
if let ExprKind::Call { func, args, .. } = &args[0].node {
if let ExprKind::Name { id: f, .. } = &func.node {
if f == "map" {
if let Some(arg) = args.first() {
if let ExprKind::Lambda { body, .. } = &arg.node {
match &body.node {
ExprKind::Tuple { elts, .. }
| ExprKind::List { elts, .. }
if elts.len() == 2 =>
{
return Some(Check::new(
CheckKind::UnnecessaryMap(id.to_string()),
Range::from_located(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,13 +1,13 @@
use log::error;
use rustpython_ast::{Expr, Stmt, StmtKind};
use crate::ast::checks;
use crate::autofix::{fixer, fixes};
use crate::autofix::{fixer, 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),
@@ -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;
@@ -15,17 +17,28 @@ pub mod cache;
pub mod check_ast;
mod check_lines;
pub mod checks;
pub mod cli;
pub mod code_gen;
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>> {
@@ -41,7 +54,7 @@ pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings = Settings::from_raw(RawSettings::from_pyproject(pyproject, project_root)?);
let settings = Settings::from_raw(RawSettings::from_pyproject(&pyproject, &project_root)?);
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);

View File

@@ -1,7 +1,10 @@
use std::fs::write;
use std::io;
use std::io::Write;
use std::path::Path;
use anyhow::Result;
#[cfg(not(target_family = "wasm"))]
use log::debug;
use rustpython_parser::lexer::LexResult;
use rustpython_parser::{lexer, parser};
@@ -75,7 +78,7 @@ pub(crate) fn check_path(
if !ignores.is_empty() {
return Ok(checks
.into_iter()
.filter(|check| !ignores.contains(check.kind.code()))
.filter(|check| !ignores.contains(&check.kind.code()))
.collect());
}
}
@@ -83,6 +86,44 @@ pub(crate) fn check_path(
Ok(checks)
}
pub fn lint_stdin(
path: &Path,
stdin: &str,
settings: &Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Message>> {
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(stdin);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Generate checks.
let mut checks = check_path(path, stdin, tokens, &noqa_line_for, settings, autofix)?;
// Apply autofix, write results to stdout.
if matches!(autofix, fixer::Mode::Apply) {
let output = match fix_file(&mut checks, stdin) {
None => stdin.to_string(),
Some(content) => content,
};
io::stdout().write_all(output.as_bytes())?;
}
// Convert to messages.
Ok(checks
.into_iter()
.map(|check| Message {
kind: check.kind,
fixed: check.fix.map(|fix| fix.applied).unwrap_or_default(),
location: check.location,
end_location: check.end_location,
filename: path.to_string_lossy().to_string(),
})
.collect())
}
#[cfg_attr(target_family = "wasm", allow(unused_variables))]
pub fn lint_path(
path: &Path,
settings: &Settings,
@@ -92,6 +133,7 @@ pub fn lint_path(
let metadata = path.metadata()?;
// Check the cache.
#[cfg(not(target_family = "wasm"))]
if let Some(messages) = cache::get(path, &metadata, settings, autofix, mode) {
debug!("Cache hit for: {}", path.to_string_lossy());
return Ok(messages);
@@ -111,7 +153,9 @@ pub fn lint_path(
// Apply autofix.
if matches!(autofix, fixer::Mode::Apply) {
fix_file(&mut checks, &contents, path)?;
if let Some(fixed_contents) = fix_file(&mut checks, &contents) {
write(path, fixed_contents)?;
}
};
// Convert to messages.
@@ -125,6 +169,7 @@ pub fn lint_path(
filename: path.to_string_lossy().to_string(),
})
.collect();
#[cfg(not(target_family = "wasm"))]
cache::set(path, &metadata, settings, autofix, &messages, mode);
Ok(messages)
@@ -171,11 +216,13 @@ pub fn autoformat_path(path: &Path) -> Result<()> {
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use regex::Regex;
use rustpython_parser::lexer::LexResult;
use test_case::test_case;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
@@ -195,459 +242,144 @@ mod tests {
linter::check_path(path, &contents, tokens, &noqa_line_for, settings, autofix)
}
#[test]
fn e402() -> Result<()> {
#[test_case(CheckCode::A001, Path::new("A001.py"); "A001")]
#[test_case(CheckCode::A002, Path::new("A002.py"); "A002")]
#[test_case(CheckCode::A003, Path::new("A003.py"); "A003")]
#[test_case(CheckCode::B011, Path::new("B011.py"); "B011")]
#[test_case(CheckCode::B014, Path::new("B014.py"); "B014")]
#[test_case(CheckCode::B025, Path::new("B025.py"); "B025")]
#[test_case(CheckCode::C400, Path::new("C400.py"); "C400")]
#[test_case(CheckCode::C401, Path::new("C401.py"); "C401")]
#[test_case(CheckCode::C402, Path::new("C402.py"); "C402")]
#[test_case(CheckCode::C403, Path::new("C403.py"); "C403")]
#[test_case(CheckCode::C404, Path::new("C404.py"); "C404")]
#[test_case(CheckCode::C405, Path::new("C405.py"); "C405")]
#[test_case(CheckCode::C406, Path::new("C406.py"); "C406")]
#[test_case(CheckCode::C408, Path::new("C408.py"); "C408")]
#[test_case(CheckCode::C409, Path::new("C409.py"); "C409")]
#[test_case(CheckCode::C410, Path::new("C410.py"); "C410")]
#[test_case(CheckCode::C411, Path::new("C411.py"); "C411")]
#[test_case(CheckCode::C413, Path::new("C413.py"); "C413")]
#[test_case(CheckCode::C414, Path::new("C414.py"); "C414")]
#[test_case(CheckCode::C415, Path::new("C415.py"); "C415")]
#[test_case(CheckCode::C416, Path::new("C416.py"); "C416")]
#[test_case(CheckCode::C417, Path::new("C417.py"); "C417")]
#[test_case(CheckCode::D100, Path::new("D.py"); "D100")]
#[test_case(CheckCode::D101, Path::new("D.py"); "D101")]
#[test_case(CheckCode::D102, Path::new("D.py"); "D102")]
#[test_case(CheckCode::D103, Path::new("D.py"); "D103")]
#[test_case(CheckCode::D104, Path::new("D.py"); "D104")]
#[test_case(CheckCode::D105, Path::new("D.py"); "D105")]
#[test_case(CheckCode::D106, Path::new("D.py"); "D106")]
#[test_case(CheckCode::D107, Path::new("D.py"); "D107")]
#[test_case(CheckCode::D201, Path::new("D.py"); "D201")]
#[test_case(CheckCode::D202, Path::new("D.py"); "D202")]
#[test_case(CheckCode::D203, Path::new("D.py"); "D203")]
#[test_case(CheckCode::D204, Path::new("D.py"); "D204")]
#[test_case(CheckCode::D205, Path::new("D.py"); "D205")]
#[test_case(CheckCode::D206, Path::new("D.py"); "D206")]
#[test_case(CheckCode::D207, Path::new("D.py"); "D207")]
#[test_case(CheckCode::D208, Path::new("D.py"); "D208")]
#[test_case(CheckCode::D209, Path::new("D.py"); "D209")]
#[test_case(CheckCode::D210, Path::new("D.py"); "D210")]
#[test_case(CheckCode::D211, Path::new("D.py"); "D211")]
#[test_case(CheckCode::D212, Path::new("D.py"); "D212")]
#[test_case(CheckCode::D213, Path::new("D.py"); "D213")]
#[test_case(CheckCode::D214, Path::new("sections.py"); "D214")]
#[test_case(CheckCode::D215, Path::new("sections.py"); "D215")]
#[test_case(CheckCode::D300, Path::new("D.py"); "D300")]
#[test_case(CheckCode::D400, Path::new("D.py"); "D400")]
#[test_case(CheckCode::D402, Path::new("D.py"); "D402")]
#[test_case(CheckCode::D403, Path::new("D.py"); "D403")]
#[test_case(CheckCode::D404, Path::new("D.py"); "D404")]
#[test_case(CheckCode::D405, Path::new("sections.py"); "D405")]
#[test_case(CheckCode::D406, Path::new("sections.py"); "D406")]
#[test_case(CheckCode::D407, Path::new("sections.py"); "D407")]
#[test_case(CheckCode::D408, Path::new("sections.py"); "D408")]
#[test_case(CheckCode::D409, Path::new("sections.py"); "D409")]
#[test_case(CheckCode::D410, Path::new("sections.py"); "D410")]
#[test_case(CheckCode::D411, Path::new("sections.py"); "D411")]
#[test_case(CheckCode::D412, Path::new("sections.py"); "D412")]
#[test_case(CheckCode::D413, Path::new("sections.py"); "D413")]
#[test_case(CheckCode::D414, Path::new("sections.py"); "D414")]
#[test_case(CheckCode::D415, Path::new("D.py"); "D415")]
#[test_case(CheckCode::D416, Path::new("D.py"); "D416")]
#[test_case(CheckCode::D417, Path::new("sections.py"); "D417_0")]
#[test_case(CheckCode::D417, Path::new("canonical_numpy_examples.py"); "D417_1")]
#[test_case(CheckCode::D417, Path::new("canonical_google_examples.py"); "D417_2")]
#[test_case(CheckCode::D418, Path::new("D.py"); "D418")]
#[test_case(CheckCode::D419, Path::new("D.py"); "D419")]
#[test_case(CheckCode::E402, Path::new("E402.py"); "E402")]
#[test_case(CheckCode::E501, Path::new("E501.py"); "E501")]
#[test_case(CheckCode::E711, Path::new("E711.py"); "E711")]
#[test_case(CheckCode::E712, Path::new("E712.py"); "E712")]
#[test_case(CheckCode::E713, Path::new("E713.py"); "E713")]
#[test_case(CheckCode::E714, Path::new("E714.py"); "E714")]
#[test_case(CheckCode::E721, Path::new("E721.py"); "E721")]
#[test_case(CheckCode::E722, Path::new("E722.py"); "E722")]
#[test_case(CheckCode::E731, Path::new("E731.py"); "E731")]
#[test_case(CheckCode::E741, Path::new("E741.py"); "E741")]
#[test_case(CheckCode::E742, Path::new("E742.py"); "E742")]
#[test_case(CheckCode::E743, Path::new("E743.py"); "E743")]
#[test_case(CheckCode::E999, Path::new("E999.py"); "E999")]
#[test_case(CheckCode::F401, Path::new("F401_0.py"); "F401_0")]
#[test_case(CheckCode::F401, Path::new("F401_1.py"); "F401_1")]
#[test_case(CheckCode::F401, Path::new("F401_2.py"); "F401_2")]
#[test_case(CheckCode::F401, Path::new("F401_3.py"); "F401_3")]
#[test_case(CheckCode::F401, Path::new("F401_4.py"); "F401_4")]
#[test_case(CheckCode::F402, Path::new("F402.py"); "F402")]
#[test_case(CheckCode::F403, Path::new("F403.py"); "F403")]
#[test_case(CheckCode::F404, Path::new("F404.py"); "F404")]
#[test_case(CheckCode::F405, Path::new("F405.py"); "F405")]
#[test_case(CheckCode::F406, Path::new("F406.py"); "F406")]
#[test_case(CheckCode::F407, Path::new("F407.py"); "F407")]
#[test_case(CheckCode::F541, Path::new("F541.py"); "F541")]
#[test_case(CheckCode::F601, Path::new("F601.py"); "F601")]
#[test_case(CheckCode::F602, Path::new("F602.py"); "F602")]
#[test_case(CheckCode::F622, Path::new("F622.py"); "F622")]
#[test_case(CheckCode::F631, Path::new("F631.py"); "F631")]
#[test_case(CheckCode::F632, Path::new("F632.py"); "F632")]
#[test_case(CheckCode::F633, Path::new("F633.py"); "F633")]
#[test_case(CheckCode::F634, Path::new("F634.py"); "F634")]
#[test_case(CheckCode::F701, Path::new("F701.py"); "F701")]
#[test_case(CheckCode::F702, Path::new("F702.py"); "F702")]
#[test_case(CheckCode::F704, Path::new("F704.py"); "F704")]
#[test_case(CheckCode::F706, Path::new("F706.py"); "F706")]
#[test_case(CheckCode::F707, Path::new("F707.py"); "F707")]
#[test_case(CheckCode::F722, Path::new("F722.py"); "F722")]
#[test_case(CheckCode::F821, Path::new("F821.py"); "F821")]
#[test_case(CheckCode::F822, Path::new("F822.py"); "F822")]
#[test_case(CheckCode::F823, Path::new("F823.py"); "F823")]
#[test_case(CheckCode::F831, Path::new("F831.py"); "F831")]
#[test_case(CheckCode::F841, Path::new("F841.py"); "F841")]
#[test_case(CheckCode::F901, Path::new("F901.py"); "F901")]
#[test_case(CheckCode::N801, Path::new("N801.py"); "N801")]
#[test_case(CheckCode::N802, Path::new("N802.py"); "N802")]
#[test_case(CheckCode::N803, Path::new("N803.py"); "N803")]
#[test_case(CheckCode::N804, Path::new("N804.py"); "N804")]
#[test_case(CheckCode::N805, Path::new("N805.py"); "N805")]
#[test_case(CheckCode::T201, Path::new("T201.py"); "T201")]
#[test_case(CheckCode::T203, Path::new("T203.py"); "T203")]
#[test_case(CheckCode::U001, Path::new("U001.py"); "U001")]
#[test_case(CheckCode::U002, Path::new("U002.py"); "U002")]
#[test_case(CheckCode::U003, Path::new("U003.py"); "U003")]
#[test_case(CheckCode::U004, Path::new("U004.py"); "U004")]
#[test_case(CheckCode::U005, Path::new("U005.py"); "U005")]
#[test_case(CheckCode::U006, Path::new("U006.py"); "U006")]
#[test_case(CheckCode::U007, Path::new("U007.py"); "U007")]
#[test_case(CheckCode::U008, Path::new("U008.py"); "U008")]
#[test_case(CheckCode::W292, Path::new("W292_0.py"); "W292_0")]
#[test_case(CheckCode::W292, Path::new("W292_1.py"); "W292_1")]
#[test_case(CheckCode::W292, Path::new("W292_2.py"); "W292_2")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/E402.py"),
&settings::Settings::for_rule(CheckCode::E402),
Path::new("./resources/test/fixtures").join(path).as_path(),
&settings::Settings::for_rule(check_code.clone()),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e501() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E501.py"),
&settings::Settings::for_rule(CheckCode::E501),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e711() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E711.py"),
&settings::Settings::for_rule(CheckCode::E711),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e712() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E712.py"),
&settings::Settings::for_rule(CheckCode::E712),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e713() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E713.py"),
&settings::Settings::for_rule(CheckCode::E713),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e721() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E721.py"),
&settings::Settings::for_rule(CheckCode::E721),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e722() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E722.py"),
&settings::Settings::for_rule(CheckCode::E722),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e714() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E714.py"),
&settings::Settings::for_rule(CheckCode::E714),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e731() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E731.py"),
&settings::Settings::for_rule(CheckCode::E731),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e741() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E741.py"),
&settings::Settings::for_rule(CheckCode::E741),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e742() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E742.py"),
&settings::Settings::for_rule(CheckCode::E742),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e743() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E743.py"),
&settings::Settings::for_rule(CheckCode::E743),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f401() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F401.py"),
&settings::Settings::for_rule(CheckCode::F401),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F402.py"),
&settings::Settings::for_rule(CheckCode::F402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f403() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F403.py"),
&settings::Settings::for_rule(CheckCode::F403),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f404() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F404.py"),
&settings::Settings::for_rule(CheckCode::F404),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f405() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F405.py"),
&settings::Settings::for_rule(CheckCode::F405),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f406() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F406.py"),
&settings::Settings::for_rule(CheckCode::F406),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f407() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F407.py"),
&settings::Settings::for_rule(CheckCode::F407),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f541() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F541.py"),
&settings::Settings::for_rule(CheckCode::F541),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f601() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F601.py"),
&settings::Settings::for_rule(CheckCode::F601),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f602() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F602.py"),
&settings::Settings::for_rule(CheckCode::F602),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f622() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F622.py"),
&settings::Settings::for_rule(CheckCode::F622),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f631() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F631.py"),
&settings::Settings::for_rule(CheckCode::F631),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f632() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F632.py"),
&settings::Settings::for_rule(CheckCode::F632),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f633() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F633.py"),
&settings::Settings::for_rule(CheckCode::F633),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f634() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F634.py"),
&settings::Settings::for_rule(CheckCode::F634),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f701() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F701.py"),
&settings::Settings::for_rule(CheckCode::F701),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f702() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F702.py"),
&settings::Settings::for_rule(CheckCode::F702),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f704() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F704.py"),
&settings::Settings::for_rule(CheckCode::F704),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f706() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F706.py"),
&settings::Settings::for_rule(CheckCode::F706),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f707() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F707.py"),
&settings::Settings::for_rule(CheckCode::F707),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f722() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F722.py"),
&settings::Settings::for_rule(CheckCode::F722),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f821() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F821.py"),
&settings::Settings::for_rule(CheckCode::F821),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f822() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F822.py"),
&settings::Settings::for_rule(CheckCode::F822),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f823() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F823.py"),
&settings::Settings::for_rule(CheckCode::F823),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f831() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F831.py"),
&settings::Settings::for_rule(CheckCode::F831),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f841() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F841.py"),
&settings::Settings::for_rule(CheckCode::F841),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
@@ -666,18 +398,6 @@ mod tests {
Ok(())
}
#[test]
fn f901() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F901.py"),
&settings::Settings::for_rule(CheckCode::F901),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn m001() -> Result<()> {
let mut checks = check_path(
@@ -690,30 +410,6 @@ mod tests {
Ok(())
}
#[test]
fn r001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/R001.py"),
&settings::Settings::for_rule(CheckCode::R001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn r002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/R002.py"),
&settings::Settings::for_rule(CheckCode::R002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn init() -> Result<()> {
let mut checks = check_path(
@@ -737,136 +433,4 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e999() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E999.py"),
&settings::Settings::for_rule(CheckCode::E999),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn a001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/A001.py"),
&settings::Settings::for_rule(CheckCode::A001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn a002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/A002.py"),
&settings::Settings::for_rule(CheckCode::A002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn a003() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/A003.py"),
&settings::Settings::for_rule(CheckCode::A003),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c403() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C403.py"),
&settings::Settings::for_rule(CheckCode::C403),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c404() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C404.py"),
&settings::Settings::for_rule(CheckCode::C404),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn spr001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/SPR001.py"),
&settings::Settings::for_rule(CheckCode::SPR001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn t201() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/T201.py"),
&settings::Settings::for_rule(CheckCode::T201),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn t203() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/T203.py"),
&settings::Settings::for_rule(CheckCode::T203),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U001.py"),
&settings::Settings::for_rule(CheckCode::U001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U002.py"),
&settings::Settings::for_rule(CheckCode::U002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -1,101 +1,52 @@
use std::io;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::Instant;
use anyhow::Result;
use clap::{command, Parser};
use clap::Parser;
use colored::Colorize;
use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use regex::Regex;
use walkdir::DirEntry;
use ::ruff::cache;
use ::ruff::checks::CheckCode;
use ::ruff::checks::CheckKind;
use ::ruff::fs::iter_python_files;
use ::ruff::linter::add_noqa_to_path;
use ::ruff::linter::lint_path;
use ::ruff::logging::set_up_logging;
use ::ruff::message::Message;
use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::pyproject::{self, StrCheckCodePair};
use ::ruff::settings::CurrentSettings;
use ::ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ::ruff::tell_user;
#[cfg(not(target_family = "wasm"))]
use ruff::cache;
use ruff::checks::CheckCode;
use ruff::checks::CheckKind;
use ruff::cli::{warn_on, Cli, Warnable};
use ruff::fs::iter_python_files;
use ruff::linter::add_noqa_to_path;
use ruff::linter::autoformat_path;
use ruff::linter::{lint_path, lint_stdin};
use ruff::logging::set_up_logging;
use ruff::message::Message;
use ruff::printer::{Printer, SerializationFormat};
use ruff::pyproject::{self};
use ruff::settings::CurrentSettings;
use ruff::settings::RawSettings;
use ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ruff::tell_user;
#[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");
#[derive(Debug, Parser)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
#[command(version)]
struct Cli {
#[arg(required = true)]
files: Vec<PathBuf>,
/// Enable verbose logging.
#[arg(short, long)]
verbose: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[arg(short, long)]
quiet: bool,
/// Exit with status code "0", even upon detecting errors.
#[arg(short, long)]
exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
watch: bool,
/// Attempt to automatically fix lint errors.
#[arg(short, long)]
fix: bool,
/// Disable cache reads.
#[arg(short, long)]
no_cache: bool,
/// List of error codes to enable.
#[arg(long, value_delimiter = ',')]
select: Vec<CheckCode>,
/// Like --select, but adds additional error codes on top of the selected ones.
#[arg(long, value_delimiter = ',')]
extend_select: Vec<CheckCode>,
/// List of error codes to ignore.
#[arg(long, value_delimiter = ',')]
ignore: Vec<CheckCode>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[arg(long, value_delimiter = ',')]
extend_ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[arg(long, value_delimiter = ',')]
exclude: Vec<String>,
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[arg(long, value_delimiter = ',')]
extend_exclude: Vec<String>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
per_file_ignores: Vec<StrCheckCodePair>,
/// Output serialization format for error messages.
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
format: SerializationFormat,
/// See the files ruff will be run against with the current settings.
#[arg(long)]
show_files: bool,
/// See ruff's settings.
#[arg(long)]
show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
#[arg(long)]
add_noqa: bool,
/// Regular expression matching the name of dummy variables.
#[arg(long)]
dummy_variable_rgx: Option<Regex>,
/// Round-trip auto-formatting.
// TODO(charlie): This should be a sub-command.
#[arg(long, hide = true)]
autoformat: bool,
/// 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")]
@@ -122,8 +73,11 @@ fn check_for_updates() {
}
}
fn show_settings(settings: RawSettings) {
println!("{:#?}", CurrentSettings::from_settings(settings));
fn show_settings(settings: RawSettings, project_root: Option<PathBuf>, pyproject: Option<PathBuf>) {
println!(
"{:#?}",
CurrentSettings::from_settings(settings, project_root, pyproject)
);
}
fn show_files(files: &[PathBuf], settings: &Settings) {
@@ -138,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,
@@ -154,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) => {
@@ -210,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();
@@ -240,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)
@@ -266,7 +230,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..."),
@@ -289,7 +255,7 @@ fn inner_main() -> Result<ExitCode> {
.map(|pair| PerFileIgnore::new(pair, &project_root))
.collect();
let mut settings = RawSettings::from_pyproject(pyproject, project_root)?;
let mut settings = RawSettings::from_pyproject(&pyproject, &project_root)?;
if !exclude.is_empty() {
settings.exclude = exclude;
}
@@ -300,9 +266,25 @@ fn inner_main() -> Result<ExitCode> {
settings.per_file_ignores = per_file_ignores;
}
if !cli.select.is_empty() {
warn_on(
Warnable::Select,
&cli.select,
&cli.ignore,
&cli.extend_ignore,
&settings,
&pyproject,
);
settings.select = cli.select;
}
if !cli.extend_select.is_empty() {
warn_on(
Warnable::ExtendSelect,
&cli.extend_select,
&cli.ignore,
&cli.extend_ignore,
&settings,
&pyproject,
);
settings.extend_select = cli.extend_select;
}
if !cli.ignore.is_empty() {
@@ -311,6 +293,9 @@ fn inner_main() -> Result<ExitCode> {
if !cli.extend_ignore.is_empty() {
settings.extend_ignore = cli.extend_ignore;
}
if let Some(target_version) = cli.target_version {
settings.target_version = target_version;
}
if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx {
settings.dummy_variable_rgx = dummy_variable_rgx;
}
@@ -320,7 +305,7 @@ fn inner_main() -> Result<ExitCode> {
return Ok(ExitCode::FAILURE);
}
if cli.show_settings {
show_settings(settings);
show_settings(settings, project_root, pyproject);
return Ok(ExitCode::SUCCESS);
}
@@ -331,6 +316,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);
@@ -396,13 +382,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.quiet && !cli.fix,
false,
)
} else {
(
run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?,
!cli.quiet,
!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

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

View File

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

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

@@ -0,0 +1,99 @@
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
}

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

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

View File

@@ -1,23 +0,0 @@
use rustpython_ast::{Expr, Location};
use crate::ast::checks;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::Fix;
pub fn assert_equals(checker: &mut Checker, expr: &Expr) {
if let Some(mut check) = checks::check_assert_equals(expr) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "assertEqual".to_string(),
location: Location::new(expr.location.row(), expr.location.column() + 1),
end_location: Location::new(
expr.location.row(),
expr.location.column() + 1 + "assertEquals".len(),
),
applied: false,
});
}
checker.add_check(check);
}
}

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