Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
138b06c98a | ||
|
|
2415d73260 | ||
|
|
b060ae2f22 | ||
|
|
9aa91d3d3c | ||
|
|
dcedb801e5 | ||
|
|
d3e7fdabb5 | ||
|
|
dfa5a4f0f7 | ||
|
|
58a2d600da | ||
|
|
7ecbfe4f6a | ||
|
|
d8248104a7 | ||
|
|
6eb09122c0 | ||
|
|
f84c1f1fa1 | ||
|
|
3aa9528229 | ||
|
|
5a3f06bab1 | ||
|
|
db59d5b558 | ||
|
|
2fcbf3ab62 | ||
|
|
fa9b10be72 | ||
|
|
c495cef529 | ||
|
|
c0c8dff6ce | ||
|
|
80b00cc89f | ||
|
|
934db3d179 | ||
|
|
6a040a0405 | ||
|
|
2821ef0f69 | ||
|
|
343d931ddb | ||
|
|
3fc257f71b | ||
|
|
6dbb0a17e9 | ||
|
|
ae5ad6a4ac | ||
|
|
549af6c584 | ||
|
|
9a799eb4e6 | ||
|
|
f260b873b6 | ||
|
|
782a90b584 | ||
|
|
7df903dc4d | ||
|
|
8fc5e91ec7 | ||
|
|
9ca1a2c273 | ||
|
|
86265c1d7c | ||
|
|
a057c9a323 | ||
|
|
2e63bb6dcb | ||
|
|
1b5db80b32 | ||
|
|
3f20cea402 | ||
|
|
389fe1ff64 | ||
|
|
bad2d7ba85 | ||
|
|
416aa298ac | ||
|
|
a535b1adbf | ||
|
|
05fbd1a283 | ||
|
|
63552cbc8e | ||
|
|
c00bd489f1 | ||
|
|
16c2e3a995 | ||
|
|
650b025181 | ||
|
|
8fe46f7400 | ||
|
|
a9bcc15797 | ||
|
|
b6c856bd07 | ||
|
|
4beea0484a | ||
|
|
2679db1d10 | ||
|
|
3e73462e04 | ||
|
|
f63a87737a | ||
|
|
e7472eac1c | ||
|
|
db3c847771 | ||
|
|
4adbfc24a5 | ||
|
|
8f734a6562 | ||
|
|
bcf7519eb3 | ||
|
|
66089052ee | ||
|
|
d50cc8ff65 | ||
|
|
b75ea94f58 | ||
|
|
2c24e2fd28 | ||
|
|
f8dc208665 | ||
|
|
c72b8e8d1e | ||
|
|
b108c693fa | ||
|
|
aac1912ea7 | ||
|
|
3dcd26aac3 | ||
|
|
e53b9807f6 | ||
|
|
36fe8b76d4 | ||
|
|
f832f88c75 | ||
|
|
659a28de02 | ||
|
|
583149a472 | ||
|
|
206e6463be | ||
|
|
118a9feec8 | ||
|
|
1f2ccb059a | ||
|
|
f4d9d6c858 | ||
|
|
3477f5664a | ||
|
|
edefa5219c | ||
|
|
cf0d198365 | ||
|
|
08b14ed77e | ||
|
|
6ee3075867 | ||
|
|
cc8a945cbf | ||
|
|
1a1922b3fc | ||
|
|
48bd766298 | ||
|
|
1ece3873cd | ||
|
|
472d902486 | ||
|
|
4ac6a18d40 | ||
|
|
8a47ea91ba | ||
|
|
bd4394aa89 | ||
|
|
56f69ce71e | ||
|
|
248a6cd50b | ||
|
|
d9e659d817 | ||
|
|
77e5564f4b | ||
|
|
e79766d5ec | ||
|
|
c55fd76743 | ||
|
|
e2aedc5ba8 | ||
|
|
1b2d085460 | ||
|
|
cb138526b1 | ||
|
|
8eac270d8f | ||
|
|
6e19fd20bb | ||
|
|
10868445f5 | ||
|
|
e3ecf21287 | ||
|
|
fd849e112e | ||
|
|
af27471c77 | ||
|
|
bb466bc8d3 | ||
|
|
7741a713e2 | ||
|
|
3ab1cfc6f8 | ||
|
|
e73f13473d | ||
|
|
3e8ef5b40f | ||
|
|
c59610906c | ||
|
|
2353a52be8 | ||
|
|
bbffdd57ff | ||
|
|
3c15c578a7 | ||
|
|
6a8e31b2ff | ||
|
|
6407fd5a33 | ||
|
|
b64040cbb2 | ||
|
|
952a0eb4e3 | ||
|
|
3e28d6de04 | ||
|
|
9bbfd1d3b2 | ||
|
|
6fb82ab763 | ||
|
|
6b286e9bc1 | ||
|
|
bcddd9e97f | ||
|
|
3e789136af | ||
|
|
07ef3b8754 | ||
|
|
46e1b16472 | ||
|
|
720bfe0161 | ||
|
|
2f69be0d41 | ||
|
|
54cb2eb15b | ||
|
|
167992ad48 | ||
|
|
f0dab24079 | ||
|
|
77055faab6 | ||
|
|
e08e1caf71 | ||
|
|
0072dfd81e | ||
|
|
6ffe02ee05 | ||
|
|
688fc0cd02 | ||
|
|
f30e5e45ab | ||
|
|
1a68a38306 | ||
|
|
590aa92ead | ||
|
|
8868f57a74 | ||
|
|
71802f8861 | ||
|
|
5b6fb8cefa | ||
|
|
2ff964107c | ||
|
|
141132d5be | ||
|
|
8ba872ece4 | ||
|
|
209dce2033 | ||
|
|
90d88dfb10 | ||
|
|
4730911b25 | ||
|
|
4e9fb9907a | ||
|
|
b8dce8922d | ||
|
|
30877127bc | ||
|
|
8b66bbdc9b | ||
|
|
71d3a84b14 | ||
|
|
323a5c857c | ||
|
|
42cec3f5a0 | ||
|
|
ee42413e10 | ||
|
|
765db12b84 | ||
|
|
e1b711d9c6 | ||
|
|
35f593846e | ||
|
|
c384fa513b | ||
|
|
022ff64d29 | ||
|
|
5a06fb28fd | ||
|
|
46750a3e17 | ||
|
|
9cc902b802 | ||
|
|
c2a36ebd1e | ||
|
|
34ca225393 | ||
|
|
38c30905e6 | ||
|
|
2774194b03 | ||
|
|
71ebd39f35 | ||
|
|
a2f78ba2c7 | ||
|
|
b51a080a44 | ||
|
|
6a1d7d8a1c | ||
|
|
10b250ee57 | ||
|
|
30b1b1e15a | ||
|
|
aafe7c0c39 | ||
|
|
f060248656 | ||
|
|
bbe0220c72 |
@@ -1,8 +1,8 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.40
|
||||
rev: v0.0.92
|
||||
hooks:
|
||||
- id: lint
|
||||
- id: ruff
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.10.1
|
||||
|
||||
450
Cargo.lock
generated
450
Cargo.lock
generated
@@ -37,6 +37,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "annotate-snippets"
|
||||
version = "0.6.1"
|
||||
@@ -64,6 +70,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"
|
||||
@@ -354,6 +374,12 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.73"
|
||||
@@ -403,15 +429,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.0.9"
|
||||
name = "ciborium"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30607dd93c420c6f1f80b544be522a0238a7db35e6a12968d28910983fee0df0"
|
||||
checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"clap_lex 0.2.4",
|
||||
"indexmap",
|
||||
"textwrap 0.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bf8832993da70a4c6d13c581f4463c2bdda27b9bf1c5498dc4365543abe6d6f"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"clap_lex 0.3.0",
|
||||
"once_cell",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
@@ -419,9 +484,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",
|
||||
@@ -430,6 +495,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.3.0"
|
||||
@@ -452,6 +526,25 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codegen"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff61280aed771c3070e7dcc9e050c66f1eb1e3b96431ba66f9f74641d02fc41d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.0.0"
|
||||
@@ -515,6 +608,42 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"atty",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap 3.2.23",
|
||||
"criterion-plot",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.6"
|
||||
@@ -574,12 +703,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 +837,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 +1113,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]]
|
||||
@@ -950,11 +1137,20 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -991,17 +1187,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 +1292,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 +1406,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 +1434,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"
|
||||
@@ -1451,6 +1673,12 @@ version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.2.3"
|
||||
@@ -1506,18 +1734,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",
|
||||
]
|
||||
@@ -1659,6 +1887,34 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "2.3.0"
|
||||
@@ -1685,6 +1941,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,25 +2190,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.63"
|
||||
version = "0.0.92"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"bincode",
|
||||
"cacache",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap 4.0.15",
|
||||
"clearscreen",
|
||||
"codegen",
|
||||
"colored",
|
||||
"common-path",
|
||||
"criterion",
|
||||
"dirs 4.0.0",
|
||||
"fern",
|
||||
"filetime",
|
||||
"getrandom 0.2.7",
|
||||
"glob",
|
||||
"insta",
|
||||
"itertools",
|
||||
"libcst",
|
||||
"log",
|
||||
"notify",
|
||||
"num-bigint",
|
||||
"once_cell",
|
||||
"path-absolutize",
|
||||
"rayon",
|
||||
@@ -1937,6 +2225,9 @@ dependencies = [
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"test-case",
|
||||
"textwrap 0.15.1",
|
||||
"titlecase",
|
||||
"toml",
|
||||
"update-informer",
|
||||
"walkdir",
|
||||
@@ -1957,7 +2248,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-ast"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
|
||||
source = "git+https://github.com/RustPython/RustPython.git?rev=77b821a1941019fe34f73ce17cea013ae1b98fd0#77b821a1941019fe34f73ce17cea013ae1b98fd0"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"rustpython-common",
|
||||
@@ -1967,7 +2258,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-common"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
|
||||
source = "git+https://github.com/RustPython/RustPython.git?rev=77b821a1941019fe34f73ce17cea013ae1b98fd0#77b821a1941019fe34f73ce17cea013ae1b98fd0"
|
||||
dependencies = [
|
||||
"ascii",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -1990,7 +2281,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-compiler-core"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
|
||||
source = "git+https://github.com/RustPython/RustPython.git?rev=77b821a1941019fe34f73ce17cea013ae1b98fd0#77b821a1941019fe34f73ce17cea013ae1b98fd0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bitflags",
|
||||
@@ -2007,7 +2298,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-parser"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
|
||||
source = "git+https://github.com/RustPython/RustPython.git?rev=77b821a1941019fe34f73ce17cea013ae1b98fd0#77b821a1941019fe34f73ce17cea013ae1b98fd0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
@@ -2055,6 +2346,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"
|
||||
@@ -2093,9 +2390,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",
|
||||
@@ -2198,6 +2495,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"
|
||||
@@ -2278,9 +2581,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.101"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
|
||||
checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2344,6 +2647,51 @@ 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 = "textwrap"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.37"
|
||||
@@ -2384,6 +2732,16 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
@@ -2399,6 +2757,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"
|
||||
@@ -2496,9 +2865,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"
|
||||
@@ -2509,6 +2888,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"
|
||||
@@ -2592,6 +2977,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"
|
||||
|
||||
36
Cargo.toml
36
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.63"
|
||||
version = "0.0.92"
|
||||
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,23 +21,39 @@ itertools = { version = "0.10.5" }
|
||||
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "32a044c127668df44582f85699358e67803b0d73" }
|
||||
log = { version = "0.4.17" }
|
||||
notify = { version = "4.0.17" }
|
||||
num-bigint = { version = "0.4.3" }
|
||||
once_cell = { version = "1.13.1" }
|
||||
path-absolutize = { version = "3.0.13", features = ["once_cell_cache"] }
|
||||
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
|
||||
rayon = { version = "1.5.3" }
|
||||
regex = { version = "1.6.0" }
|
||||
rustpython-ast = { features = ["unparse"], git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
|
||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
|
||||
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
|
||||
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "77b821a1941019fe34f73ce17cea013ae1b98fd0" }
|
||||
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "77b821a1941019fe34f73ce17cea013ae1b98fd0" }
|
||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "77b821a1941019fe34f73ce17cea013ae1b98fd0" }
|
||||
serde = { version = "1.0.143", features = ["derive"] }
|
||||
serde_json = { version = "1.0.83" }
|
||||
strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.24.3" }
|
||||
textwrap = { version = "0.15.1" }
|
||||
titlecase = { version = "2.2.1" }
|
||||
toml = { version = "0.5.9" }
|
||||
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true }
|
||||
walkdir = { version = "2.3.2" }
|
||||
strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||
strum_macros = "0.24.3"
|
||||
|
||||
[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" }
|
||||
codegen = { version = "0.2.0" }
|
||||
criterion = { version = "0.4.0" }
|
||||
insta = { version = "1.19.1", features = ["yaml"] }
|
||||
test-case = { version = "2.2.2" }
|
||||
|
||||
[features]
|
||||
default = ["update-informer"]
|
||||
@@ -56,3 +70,7 @@ opt-level = 3
|
||||
|
||||
[profile.dev.package.similar]
|
||||
opt-level = 3
|
||||
|
||||
[[bench]]
|
||||
name = "source_code_locator"
|
||||
harness = false
|
||||
|
||||
574
README.md
574
README.md
@@ -1,4 +1,4 @@
|
||||
# ruff
|
||||
# Ruff
|
||||
|
||||
[](https://pypi.python.org/pypi/ruff)
|
||||
[](https://pypi.python.org/pypi/ruff)
|
||||
@@ -20,17 +20,49 @@ 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)
|
||||
1. [Pyflakes](#pyflakes)
|
||||
2. [pycodestyle (error)](#pycodestyle-error)
|
||||
3. [pycodestyle (warning)](#pycodestyle-warning)
|
||||
4. [pydocstyle](#pydocstyle)
|
||||
5. [pyupgrade](#pyupgrade)
|
||||
6. [pep8-naming](#pep8-naming)
|
||||
7. [flake8-comprehensions](#flake8-comprehensions)
|
||||
8. [flake8-bugbear](#flake8-bugbear)
|
||||
9. [flake8-builtins](#flake8-builtins)
|
||||
10. [flake8-print](#flake8-print)
|
||||
11. [flake8-quotes](#flake8-quotes)
|
||||
12. [Meta rules](#meta-rules)
|
||||
5. [Editor Integrations](#editor-integrations)
|
||||
6. [FAQ](#faq)
|
||||
7. [Development](#development)
|
||||
8. [Releases](#releases)
|
||||
9. [Benchmarks](#benchmarks)
|
||||
10. [License](#license)
|
||||
11. [Contributing](#contributing)
|
||||
|
||||
## Installation and Usage
|
||||
|
||||
### 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 +70,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,38 +78,53 @@ 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.92
|
||||
hooks:
|
||||
- id: lint
|
||||
- id: ruff
|
||||
```
|
||||
|
||||
<!-- TODO(charlie): Remove this message a few versions after v0.0.86. -->
|
||||
_Note: prior to `v0.0.86`, `ruff-pre-commit` used `lint` (rather than `ruff`) as the hook ID._
|
||||
|
||||
## Configuration
|
||||
|
||||
ruff is configurable both via `pyproject.toml` and the command line.
|
||||
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]
|
||||
line-length = 88
|
||||
select = [
|
||||
"F401",
|
||||
"F403",
|
||||
select = ["E", "F"]
|
||||
ignore = ["E501"]
|
||||
per-file-ignores = [
|
||||
"__init__.py:F401",
|
||||
"path/to/file.py:F401"
|
||||
]
|
||||
```
|
||||
|
||||
Alternatively, on the command-line:
|
||||
Plugin configurations should be expressed as subsections, e.g.:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
```
|
||||
|
||||
Alternatively, common configuration settings can be provided via the command-line:
|
||||
|
||||
```shell
|
||||
ruff path/to/code/ --select F401 --select F403
|
||||
@@ -94,6 +141,8 @@ Arguments:
|
||||
<FILES>...
|
||||
|
||||
Options:
|
||||
--config <CONFIG>
|
||||
Path to the `pyproject.toml` file to use for configuration
|
||||
-v, --verbose
|
||||
Enable verbose logging
|
||||
-q, --quiet
|
||||
@@ -132,6 +181,8 @@ Options:
|
||||
Regular expression matching the name of dummy variables
|
||||
--target-version <TARGET_VERSION>
|
||||
The minimum Python version that should be supported
|
||||
--stdin-filename <STDIN_FILENAME>
|
||||
The name of the file when passing it through stdin
|
||||
-h, --help
|
||||
Print help information
|
||||
-V, --version
|
||||
@@ -154,7 +205,7 @@ Exclusions are based on globs, and can be either:
|
||||
To omit a lint check entirely, add it to the "ignore" list via `--ignore` or `--extend-ignore`,
|
||||
either on the command-line or in your `project.toml` file.
|
||||
|
||||
To ignore an error in-line, ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
|
||||
To ignore an error in-line, Ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
|
||||
To ignore an individual error, add `# noqa: {code}` to the end of the line, like so:
|
||||
|
||||
```python
|
||||
@@ -178,143 +229,249 @@ 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` and `F` error codes, which correspond to those built-in to Flake8.
|
||||
|
||||
The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` command-line option.
|
||||
|
||||
| Code | Name | Message | | |
|
||||
| ---- | ---- | ------- | --- | --- |
|
||||
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | ✅ | |
|
||||
| E501 | LineTooLong | Line too long (89 > 88 characters) | ✅ | |
|
||||
| E711 | NoneComparison | Comparison to `None` should be `cond is None` | ✅ | |
|
||||
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | ✅ | |
|
||||
| E713 | NotInTest | Test for membership should be `not in` | ✅ | |
|
||||
| E714 | NotIsTest | Test for object identity should be `is not` | ✅ | |
|
||||
| E721 | TypeComparison | Do not compare types, use `isinstance()` | ✅ | |
|
||||
| E722 | DoNotUseBareExcept | Do not use bare `except` | ✅ | |
|
||||
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | ✅ | |
|
||||
| E741 | AmbiguousVariableName | Ambiguous variable name: `...` | ✅ | |
|
||||
| E742 | AmbiguousClassName | Ambiguous class name: `...` | ✅ | |
|
||||
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | ✅ | |
|
||||
| E902 | IOError | IOError: `...` | ✅ | |
|
||||
| E999 | SyntaxError | SyntaxError: `...` | ✅ | |
|
||||
| W292 | NoNewLineAtEndOfFile | No newline at end of file | ✅ | |
|
||||
| F401 | UnusedImport | `...` imported but unused | ✅ | 🛠 |
|
||||
| F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable | ✅ | |
|
||||
| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names | ✅ | |
|
||||
| F404 | LateFutureImport | `from __future__` imports must occur at the beginning of the file | ✅ | |
|
||||
| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | ✅ | |
|
||||
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | ✅ | |
|
||||
| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | ✅ | |
|
||||
| F541 | FStringMissingPlaceholders | f-string without any placeholders | ✅ | |
|
||||
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | ✅ | |
|
||||
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | ✅ | |
|
||||
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | ✅ | |
|
||||
| F622 | TwoStarredExpressions | Two starred expressions in assignment | ✅ | |
|
||||
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | ✅ | |
|
||||
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | ✅ | |
|
||||
| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | ✅ | |
|
||||
| F634 | IfTuple | If test is a tuple, which is always `True` | ✅ | |
|
||||
| F701 | BreakOutsideLoop | `break` outside loop | ✅ | |
|
||||
| F702 | ContinueOutsideLoop | `continue` not properly in loop | ✅ | |
|
||||
| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function/method | ✅ | |
|
||||
| F706 | ReturnOutsideFunction | `return` statement outside of a function/method | ✅ | |
|
||||
| F707 | DefaultExceptNotLast | An `except:` block as not the last exception handler | ✅ | |
|
||||
| F722 | ForwardAnnotationSyntaxError | Syntax error in forward annotation: `...` | ✅ | |
|
||||
| F821 | UndefinedName | Undefined name `...` | ✅ | |
|
||||
| F822 | UndefinedExport | Undefined name `...` in `__all__` | ✅ | |
|
||||
| F823 | UndefinedLocal | Local variable `...` referenced before assignment | ✅ | |
|
||||
| F831 | DuplicateArgumentName | Duplicate argument name in function definition | ✅ | |
|
||||
| F841 | UnusedVariable | Local variable `...` is assigned to but never used | ✅ | |
|
||||
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | ✅ | |
|
||||
| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin | | |
|
||||
| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin | | |
|
||||
| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin | | |
|
||||
| C400 | UnnecessaryGeneratorList | Unnecessary generator - rewrite as a list comprehension | | |
|
||||
| C401 | UnnecessaryGeneratorSet | Unnecessary generator - rewrite as a set comprehension | | |
|
||||
| C402 | UnnecessaryGeneratorDict | Unnecessary generator - rewrite as a dict comprehension | | |
|
||||
| C403 | UnnecessaryListComprehensionSet | Unnecessary list comprehension - rewrite as a set comprehension | | |
|
||||
| C404 | UnnecessaryListComprehensionDict | Unnecessary list comprehension - rewrite as a dict comprehension | | |
|
||||
| C405 | UnnecessaryLiteralSet | Unnecessary <list/tuple> literal - rewrite as a set literal | | |
|
||||
| C406 | UnnecessaryLiteralDict | Unnecessary <list/tuple> literal - rewrite as a dict literal | | |
|
||||
| C408 | UnnecessaryCollectionCall | Unnecessary <dict/list/tuple> call - rewrite as a literal | | |
|
||||
| SPR001 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | | 🛠 |
|
||||
| T201 | PrintFound | `print` found | | 🛠 |
|
||||
| T203 | PPrintFound | `pprint` found | | 🛠 |
|
||||
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | | 🛠 |
|
||||
| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | | 🛠 |
|
||||
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | | 🛠 |
|
||||
| U004 | UselessObjectInheritance | Class `...` inherits from object | | 🛠 |
|
||||
| U005 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 |
|
||||
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | | 🛠 |
|
||||
| U007 | UsePEP604Annotation | Use `X | Y` for type annotations | | 🛠 |
|
||||
| M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 |
|
||||
### Pyflakes
|
||||
|
||||
## Integrations
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| F401 | UnusedImport | `...` imported but unused | 🛠 |
|
||||
| F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable | |
|
||||
| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names | |
|
||||
| F404 | LateFutureImport | `from __future__` imports must occur at the beginning of the file | |
|
||||
| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | |
|
||||
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | |
|
||||
| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | |
|
||||
| F541 | FStringMissingPlaceholders | f-string without any placeholders | |
|
||||
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | |
|
||||
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | |
|
||||
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | |
|
||||
| F622 | TwoStarredExpressions | Two starred expressions in assignment | |
|
||||
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | |
|
||||
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | |
|
||||
| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | |
|
||||
| F634 | IfTuple | If test is a tuple, which is always `True` | |
|
||||
| F701 | BreakOutsideLoop | `break` outside loop | |
|
||||
| F702 | ContinueOutsideLoop | `continue` not properly in loop | |
|
||||
| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function | |
|
||||
| F706 | ReturnOutsideFunction | `return` statement outside of a function/method | |
|
||||
| F707 | DefaultExceptNotLast | An `except:` block as not the last exception handler | |
|
||||
| F722 | ForwardAnnotationSyntaxError | Syntax error in forward annotation: `...` | |
|
||||
| F821 | UndefinedName | Undefined name `...` | |
|
||||
| F822 | UndefinedExport | Undefined name `...` in `__all__` | |
|
||||
| F823 | UndefinedLocal | Local variable `...` referenced before assignment | |
|
||||
| F831 | DuplicateArgumentName | Duplicate argument name in function definition | |
|
||||
| F841 | UnusedVariable | Local variable `...` is assigned to but never used | |
|
||||
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | |
|
||||
|
||||
### pycodestyle (error)
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | |
|
||||
| E501 | LineTooLong | Line too long (89 > 88 characters) | |
|
||||
| E711 | NoneComparison | Comparison to `None` should be `cond is None` | |
|
||||
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | |
|
||||
| E713 | NotInTest | Test for membership should be `not in` | |
|
||||
| E714 | NotIsTest | Test for object identity should be `is not` | |
|
||||
| E721 | TypeComparison | Do not compare types, use `isinstance()` | |
|
||||
| E722 | DoNotUseBareExcept | Do not use bare `except` | |
|
||||
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | |
|
||||
| E741 | AmbiguousVariableName | Ambiguous variable name: `...` | |
|
||||
| E742 | AmbiguousClassName | Ambiguous class name: `...` | |
|
||||
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | |
|
||||
| E902 | IOError | IOError: `...` | |
|
||||
| E999 | SyntaxError | SyntaxError: `...` | |
|
||||
|
||||
### pycodestyle (warning)
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| W292 | NoNewLineAtEndOfFile | No newline at end of file | |
|
||||
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | |
|
||||
|
||||
### pydocstyle
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| D100 | PublicModule | Missing docstring in public module | |
|
||||
| D101 | PublicClass | Missing docstring in public class | |
|
||||
| D102 | PublicMethod | Missing docstring in public method | |
|
||||
| D103 | PublicFunction | Missing docstring in public function | |
|
||||
| D104 | PublicPackage | Missing docstring in public package | |
|
||||
| D105 | MagicMethod | Missing docstring in magic method | |
|
||||
| D106 | PublicNestedClass | Missing docstring in public nested class | |
|
||||
| D107 | PublicInit | Missing docstring in `__init__` | |
|
||||
| D200 | FitsOnOneLine | One-line docstring should fit on one line | |
|
||||
| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | 🛠 |
|
||||
| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | 🛠 |
|
||||
| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring | 🛠 |
|
||||
| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | 🛠 |
|
||||
| D205 | BlankLineAfterSummary | 1 blank line required between summary line and description | 🛠 |
|
||||
| D206 | IndentWithSpaces | Docstring should be indented with spaces, not tabs | |
|
||||
| D207 | NoUnderIndentation | Docstring is under-indented | 🛠 |
|
||||
| D208 | NoOverIndentation | Docstring is over-indented | 🛠 |
|
||||
| D209 | NewLineAfterLastParagraph | Multi-line docstring closing quotes should be on a separate line | 🛠 |
|
||||
| D210 | NoSurroundingWhitespace | No whitespaces allowed surrounding docstring text | 🛠 |
|
||||
| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | 🛠 |
|
||||
| D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line | |
|
||||
| D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | |
|
||||
| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | 🛠 |
|
||||
| D215 | SectionUnderlineNotOverIndented | Section underline is over-indented ("Returns") | 🛠 |
|
||||
| D300 | UsesTripleQuotes | Use """triple double quotes""" | |
|
||||
| D400 | EndsInPeriod | First line should end with a period | |
|
||||
| D402 | NoSignature | First line should not be the function's signature | |
|
||||
| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | |
|
||||
| D404 | NoThisPrefix | First word of the docstring should not be 'This' | |
|
||||
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | 🛠 |
|
||||
| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | 🛠 |
|
||||
| D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | 🛠 |
|
||||
| D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") | 🛠 |
|
||||
| D409 | SectionUnderlineMatchesSectionLength | Section underline should match the length of its name ("Returns") | 🛠 |
|
||||
| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | 🛠 |
|
||||
| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | 🛠 |
|
||||
| D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") | 🛠 |
|
||||
| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | 🛠 |
|
||||
| D414 | NonEmptySection | Section has no content ("Returns") | |
|
||||
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | |
|
||||
| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | 🛠 |
|
||||
| D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` | |
|
||||
| D418 | SkipDocstring | Function decorated with `@overload` shouldn't contain a docstring | |
|
||||
| D419 | NonEmpty | Docstring is empty | |
|
||||
|
||||
### pyupgrade
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | 🛠 |
|
||||
| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | 🛠 |
|
||||
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | 🛠 |
|
||||
| U004 | UselessObjectInheritance | Class `...` inherits from object | 🛠 |
|
||||
| U005 | DeprecatedUnittestAlias | `assertEquals` is deprecated, use `assertEqual` instead | 🛠 |
|
||||
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | 🛠 |
|
||||
| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | 🛠 |
|
||||
| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | 🛠 |
|
||||
|
||||
### pep8-naming
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| N801 | InvalidClassName | Class name `...` should use CapWords convention | |
|
||||
| N802 | InvalidFunctionName | Function name `...` should be lowercase | |
|
||||
| N803 | InvalidArgumentName | Argument name `...` should be lowercase | |
|
||||
| N804 | InvalidFirstArgumentNameForClassMethod | First argument of a class method should be named `cls` | |
|
||||
| N805 | InvalidFirstArgumentNameForMethod | First argument of a method should be named `self` | |
|
||||
| N806 | NonLowercaseVariableInFunction | Variable `...` in function should be lowercase | |
|
||||
| N807 | DunderFunctionName | Function name should not start and end with `__` | |
|
||||
| N811 | ConstantImportedAsNonConstant | Constant `...` imported as non-constant `...` | |
|
||||
| N812 | LowercaseImportedAsNonLowercase | Lowercase `...` imported as non-lowercase `...` | |
|
||||
| N813 | CamelcaseImportedAsLowercase | Camelcase `...` imported as lowercase `...` | |
|
||||
| N814 | CamelcaseImportedAsConstant | Camelcase `...` imported as constant `...` | |
|
||||
| N815 | MixedCaseVariableInClassScope | Variable `mixedCase` in class scope should not be mixedCase | |
|
||||
| N816 | MixedCaseVariableInGlobalScope | Variable `mixedCase` in global scope should not be mixedCase | |
|
||||
| N817 | CamelcaseImportedAsAcronym | Camelcase `...` imported as acronym `...` | |
|
||||
| N818 | ErrorSuffixOnExceptionName | Exception name `...` should be named with an Error suffix | |
|
||||
|
||||
### flake8-comprehensions
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| C400 | UnnecessaryGeneratorList | Unnecessary generator (rewrite as a `list` comprehension) | |
|
||||
| C401 | UnnecessaryGeneratorSet | Unnecessary generator (rewrite as a `set` comprehension) | |
|
||||
| C402 | UnnecessaryGeneratorDict | Unnecessary generator (rewrite as a `dict` comprehension) | |
|
||||
| C403 | UnnecessaryListComprehensionSet | Unnecessary `list` comprehension (rewrite as a `set` comprehension) | |
|
||||
| C404 | UnnecessaryListComprehensionDict | Unnecessary `list` comprehension (rewrite as a `dict` comprehension) | |
|
||||
| C405 | UnnecessaryLiteralSet | Unnecessary `(list\|tuple)` literal (rewrite as a `set` literal) | |
|
||||
| C406 | UnnecessaryLiteralDict | Unnecessary `(list\|tuple)` literal (rewrite as a `dict` literal) | |
|
||||
| C408 | UnnecessaryCollectionCall | Unnecessary `(dict\|list\|tuple)` call (rewrite as a literal) | |
|
||||
| C409 | UnnecessaryLiteralWithinTupleCall | Unnecessary `(list\|tuple)` literal passed to `tuple()` (remove the outer call to `tuple()`) | |
|
||||
| C410 | UnnecessaryLiteralWithinListCall | Unnecessary `(list\|tuple)` literal passed to `list()` (rewrite as a `list` literal) | |
|
||||
| C411 | UnnecessaryListCall | Unnecessary `list` call (remove the outer call to `list()`) | |
|
||||
| C413 | UnnecessaryCallAroundSorted | Unnecessary `(list\|reversed)` call around `sorted()` | |
|
||||
| C414 | UnnecessaryDoubleCastOrProcess | Unnecessary `(list\|reversed\|set\|sorted\|tuple)` call within `(list\|set\|sorted\|tuple)()` | |
|
||||
| C415 | UnnecessarySubscriptReversal | Unnecessary subscript reversal of iterable within `(reversed\|set\|sorted)()` | |
|
||||
| C416 | UnnecessaryComprehension | Unnecessary `(list\|set)` comprehension (rewrite using `(list\|set)()`) | |
|
||||
| C417 | UnnecessaryMap | Unnecessary `map` usage (rewrite using a `(list\|set\|dict)` comprehension) | |
|
||||
|
||||
### flake8-bugbear
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| B002 | UnaryPrefixIncrement | Python does not support the unary prefix increment. | |
|
||||
| B006 | MutableArgumentDefault | Do not use mutable data structures for argument defaults. | |
|
||||
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body. | 🛠 |
|
||||
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
|
||||
| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError:` instead of `except (ValueError,):`. | |
|
||||
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 |
|
||||
| B017 | NoAssertRaisesException | `assertRaises(Exception):` should be considered evil. | |
|
||||
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
|
||||
|
||||
### flake8-builtins
|
||||
|
||||
| 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 | 🛠 |
|
||||
|
||||
### flake8-quotes
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| Q000 | BadQuotesInlineString | Single quotes found but double quotes preferred | |
|
||||
| Q001 | BadQuotesMultilineString | Single quote multiline found but double quotes preferred | |
|
||||
| Q002 | BadQuotesDocstring | Single quote docstring found but double quotes preferred | |
|
||||
| Q003 | AvoidQuoteEscape | Change outer quotes to avoid escaping inner quotes | |
|
||||
|
||||
### Meta rules
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| 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:
|
||||
|
||||

|
||||

|
||||
|
||||
ruff should then appear as a runnable action:
|
||||
Ruff should then appear as a runnable action:
|
||||
|
||||

|
||||

|
||||
|
||||
### 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
|
||||
@@ -332,13 +489,158 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install ruff
|
||||
- name: Run ruff
|
||||
- name: Run Ruff
|
||||
run: ruff .
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Is Ruff compatible with Black?
|
||||
|
||||
Yes. Ruff is compatible with [Black](https://github.com/psf/black) out-of-the-box, as long as
|
||||
the `line-length` setting is consistent between the two.
|
||||
|
||||
As a project, Ruff is designed to be used alongside Black and, as such, will defer implementing
|
||||
stylistic lint rules that are obviated by autoformatting.
|
||||
|
||||
### How does Ruff compare to Flake8?
|
||||
|
||||
Ruff can be used as a (near) drop-in replacement for Flake8 when used (1) without or with a small
|
||||
number of plugins, (2) alongside Black, and (3) on Python 3 code.
|
||||
|
||||
Under those conditions Ruff is missing 14 rules related to string `.format` calls, 1 rule related
|
||||
to docstring parsing, and 1 rule related to redefined variables.
|
||||
|
||||
Ruff re-implements some of the most popular Flake8 plugins and related code quality tools natively,
|
||||
including:
|
||||
|
||||
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
|
||||
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
|
||||
- [`yesqa`](https://github.com/asottile/yesqa)
|
||||
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
|
||||
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
|
||||
- [`flake8-super`](https://pypi.org/project/flake8-super/)
|
||||
- [`flake8-print`](https://pypi.org/project/flake8-print/)
|
||||
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (10/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:
|
||||
|
||||
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
|
||||
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
|
||||
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
|
||||
- [`flake8-super`](https://pypi.org/project/flake8-super/)
|
||||
- [`flake8-print`](https://pypi.org/project/flake8-print/)
|
||||
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (10/32)
|
||||
|
||||
Ruff also implements the functionality that you get from [`yesqa`](https://github.com/asottile/yesqa),
|
||||
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34).
|
||||
|
||||
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.
|
||||
|
||||
### Do I need to install Rust to use Ruff?
|
||||
|
||||
Nope! Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
|
||||
|
||||
```shell
|
||||
pip install ruff
|
||||
```
|
||||
|
||||
Ruff ships with wheels for all major platforms, which enables `pip` to install Ruff without relying
|
||||
on Rust at all.
|
||||
|
||||
### Can I write my own plugins for Ruff?
|
||||
|
||||
Ruff does not yet support third-party plugins, though a plugin system is within-scope for the
|
||||
project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
|
||||
|
||||
### Does Ruff support NumPy- or Google-style docstrings?
|
||||
|
||||
Yes! To enable a specific docstring convention, start by enabling all `pydocstyle` error codes, and
|
||||
then selectively disabling based on your [preferred convention](https://www.pydocstyle.org/en/latest/error_codes.html#default-conventions).
|
||||
|
||||
For example, if you're coming from `flake8-docstrings`, the following configuration is equivalent to
|
||||
`--docstring-convention=numpy`:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-select = ["D"]
|
||||
extend-ignore = [
|
||||
"D107",
|
||||
"D203",
|
||||
"D212",
|
||||
"D213",
|
||||
"D402",
|
||||
"D413",
|
||||
"D415",
|
||||
"D416",
|
||||
"D417",
|
||||
]
|
||||
```
|
||||
|
||||
Similarly, the following is equivalent to `--docstring-convention=google`:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-select = ["D"]
|
||||
extend-ignore = [
|
||||
"D203",
|
||||
"D204",
|
||||
"D213",
|
||||
"D215",
|
||||
"D400",
|
||||
"D404",
|
||||
"D406",
|
||||
"D407",
|
||||
"D408",
|
||||
"D409",
|
||||
"D413",
|
||||
]
|
||||
```
|
||||
|
||||
Similarly, the following is equivalent to `--docstring-convention=pep8`:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-select = ["D"]
|
||||
extend-ignore = [
|
||||
"D203",
|
||||
"D212",
|
||||
"D213",
|
||||
"D214",
|
||||
"D215",
|
||||
"D404",
|
||||
"D405",
|
||||
"D406",
|
||||
"D407",
|
||||
"D408",
|
||||
"D409",
|
||||
"D410",
|
||||
"D411",
|
||||
"D413",
|
||||
"D415",
|
||||
"D416",
|
||||
"D417",
|
||||
]
|
||||
```
|
||||
|
||||
## 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:
|
||||
@@ -350,13 +652,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.
|
||||
@@ -433,9 +735,9 @@ hyperfine --ignore-failure --warmup 5 \
|
||||
|
||||
In order, these evaluate:
|
||||
|
||||
- ruff
|
||||
- Ruff
|
||||
- Pylint
|
||||
- PyFlakes
|
||||
- Pyflakes
|
||||
- autoflake
|
||||
- pycodestyle
|
||||
- Flake8
|
||||
|
||||
16
benches/source_code_locator.rs
Normal file
16
benches/source_code_locator.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use ruff::fs;
|
||||
use ruff::source_code_locator::compute_offsets;
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let contents = fs::read_file(Path::new("resources/test/fixtures/D.py")).unwrap();
|
||||
c.bench_function("compute_offsets", |b| {
|
||||
b.iter(|| compute_offsets(black_box(&contents)))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
105
examples/generate_check_code_prefix.rs
Normal file
105
examples/generate_check_code_prefix.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
//! Generate the CheckCodePrefix enum.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use codegen::{Scope, Type, Variant};
|
||||
use itertools::Itertools;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use ruff::checks::CheckCode;
|
||||
|
||||
fn main() {
|
||||
// Build up a map from prefix to matching CheckCodes.
|
||||
let mut prefix_to_codes: BTreeMap<String, BTreeSet<CheckCode>> = Default::default();
|
||||
for check_code in CheckCode::iter() {
|
||||
let as_ref = check_code.as_ref().to_string();
|
||||
for i in 1..=as_ref.len() {
|
||||
let prefix = as_ref[..i].to_string();
|
||||
let entry = prefix_to_codes
|
||||
.entry(prefix)
|
||||
.or_insert_with(|| Default::default());
|
||||
entry.insert(check_code.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Create the `CheckCodePrefix` definition.
|
||||
let mut gen = scope
|
||||
.new_enum("CheckCodePrefix")
|
||||
.vis("pub")
|
||||
.derive("EnumString")
|
||||
.derive("Debug")
|
||||
.derive("PartialEq")
|
||||
.derive("Eq")
|
||||
.derive("Clone")
|
||||
.derive("Serialize")
|
||||
.derive("Deserialize");
|
||||
for (prefix, _) in &prefix_to_codes {
|
||||
gen = gen.push_variant(Variant::new(prefix.to_string()));
|
||||
}
|
||||
|
||||
// Create the `PrefixSpecificity` definition.
|
||||
scope
|
||||
.new_enum("PrefixSpecificity")
|
||||
.vis("pub")
|
||||
.derive("PartialEq")
|
||||
.derive("Eq")
|
||||
.derive("PartialOrd")
|
||||
.derive("Ord")
|
||||
.push_variant(Variant::new("Category"))
|
||||
.push_variant(Variant::new("Hundreds"))
|
||||
.push_variant(Variant::new("Tens"))
|
||||
.push_variant(Variant::new("Explicit"));
|
||||
|
||||
// Create the `match` statement, to map from definition to relevant codes.
|
||||
let mut gen = scope
|
||||
.new_impl("CheckCodePrefix")
|
||||
.new_fn("codes")
|
||||
.arg_ref_self()
|
||||
.ret(Type::new("Vec<CheckCode>"))
|
||||
.vis("pub")
|
||||
.line("match self {");
|
||||
for (prefix, codes) in &prefix_to_codes {
|
||||
gen = gen.line(format!(
|
||||
"CheckCodePrefix::{prefix} => vec![{}],",
|
||||
codes
|
||||
.iter()
|
||||
.map(|code| format!("CheckCode::{}", code.as_ref()))
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
gen.line("}");
|
||||
|
||||
// Create the `match` statement, to map from definition to specificity.
|
||||
let mut gen = scope
|
||||
.new_impl("CheckCodePrefix")
|
||||
.new_fn("specificity")
|
||||
.arg_ref_self()
|
||||
.ret(Type::new("PrefixSpecificity"))
|
||||
.vis("pub")
|
||||
.line("match self {");
|
||||
for (prefix, _) in &prefix_to_codes {
|
||||
let specificity = match prefix.len() {
|
||||
4 => "Explicit",
|
||||
3 => "Tens",
|
||||
2 => "Hundreds",
|
||||
1 => "Category",
|
||||
_ => panic!("Invalid prefix: {}", prefix),
|
||||
};
|
||||
gen = gen.line(format!(
|
||||
"CheckCodePrefix::{prefix} => PrefixSpecificity::{},",
|
||||
specificity
|
||||
));
|
||||
}
|
||||
gen.line("}");
|
||||
|
||||
println!("//! File automatically generated by examples/generate_check_code_prefix.rs.");
|
||||
println!();
|
||||
println!("use serde::{{Deserialize, Serialize}};");
|
||||
println!("use strum_macros::EnumString;");
|
||||
println!();
|
||||
println!("use crate::checks::CheckCode;");
|
||||
println!();
|
||||
println!("{}", scope.to_string());
|
||||
}
|
||||
@@ -1,26 +1,29 @@
|
||||
/// Generate a Markdown-compatible table of supported lint rules.
|
||||
//! Generate a Markdown-compatible table of supported lint rules.
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use ruff::checks::{CheckCode, DEFAULT_CHECK_CODES};
|
||||
use ruff::checks::{CheckCategory, CheckCode};
|
||||
|
||||
fn main() {
|
||||
println!("| Code | Name | Message | | |");
|
||||
println!("| ---- | ---- | ------- | --- | --- |");
|
||||
for check_code in CheckCode::iter() {
|
||||
let check_kind = check_code.kind();
|
||||
let default_token = if DEFAULT_CHECK_CODES.contains(&check_code) {
|
||||
"✅"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let fix_token = if check_kind.fixable() { "🛠" } else { "" };
|
||||
println!(
|
||||
"| {} | {} | {} | {} | {} |",
|
||||
check_kind.code().as_ref(),
|
||||
check_kind.as_ref(),
|
||||
check_kind.body(),
|
||||
default_token,
|
||||
fix_token
|
||||
);
|
||||
for check_category in CheckCategory::iter() {
|
||||
println!("### {}", check_category.title());
|
||||
println!();
|
||||
|
||||
println!("| Code | Name | Message | Fix |");
|
||||
println!("| ---- | ---- | ------- | --- |");
|
||||
for check_code in CheckCode::iter() {
|
||||
if check_code.category() == check_category {
|
||||
let check_kind = check_code.kind();
|
||||
let fix_token = if check_kind.fixable() { "🛠" } else { "" };
|
||||
println!(
|
||||
"| {} | {} | {} | {} |",
|
||||
check_kind.code().as_ref(),
|
||||
check_kind.as_ref(),
|
||||
check_kind.summary().replace("|", r"\|"),
|
||||
fix_token
|
||||
);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
5
foo.py
Normal file
5
foo.py
Normal file
@@ -0,0 +1,5 @@
|
||||
def function_with_nesting():
|
||||
"""Foo bar documentation."""
|
||||
@overload
|
||||
def nested_overloaded_func(a: int) -> str:
|
||||
...
|
||||
20
resources/test/fixtures/B002.py
vendored
Normal file
20
resources/test/fixtures/B002.py
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Should emit:
|
||||
B002 - on lines 15 and 20
|
||||
"""
|
||||
|
||||
|
||||
def this_is_all_fine(n):
|
||||
x = n + 1
|
||||
y = 1 + n
|
||||
z = +x + y
|
||||
return +z
|
||||
|
||||
|
||||
def this_is_buggy(n):
|
||||
x = ++n
|
||||
return x
|
||||
|
||||
|
||||
def this_is_buggy_too(n):
|
||||
return ++n
|
||||
187
resources/test/fixtures/B006_B008.py
vendored
Normal file
187
resources/test/fixtures/B006_B008.py
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
import collections
|
||||
import datetime as dt
|
||||
import logging
|
||||
import operator
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import types
|
||||
from operator import attrgetter, itemgetter, methodcaller
|
||||
from types import MappingProxyType
|
||||
|
||||
|
||||
# B006
|
||||
# Allow immutable literals/calls/comprehensions
|
||||
def this_is_okay(value=(1, 2, 3)):
|
||||
...
|
||||
|
||||
|
||||
async def and_this_also(value=tuple()):
|
||||
pass
|
||||
|
||||
|
||||
def frozenset_also_okay(value=frozenset()):
|
||||
pass
|
||||
|
||||
|
||||
def mappingproxytype_okay(
|
||||
value=MappingProxyType({}), value2=types.MappingProxyType({})
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def re_compile_ok(value=re.compile("foo")):
|
||||
pass
|
||||
|
||||
|
||||
def operators_ok(
|
||||
v=operator.attrgetter("foo"),
|
||||
v2=operator.itemgetter("foo"),
|
||||
v3=operator.methodcaller("foo"),
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def operators_ok_unqualified(
|
||||
v=attrgetter("foo"),
|
||||
v2=itemgetter("foo"),
|
||||
v3=methodcaller("foo"),
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def kwonlyargs_immutable(*, value=()):
|
||||
...
|
||||
|
||||
|
||||
# Flag mutable literals/comprehensions
|
||||
|
||||
|
||||
def this_is_wrong(value=[1, 2, 3]):
|
||||
...
|
||||
|
||||
|
||||
def this_is_also_wrong(value={}):
|
||||
...
|
||||
|
||||
|
||||
def and_this(value=set()):
|
||||
...
|
||||
|
||||
|
||||
def this_too(value=collections.OrderedDict()):
|
||||
...
|
||||
|
||||
|
||||
async def async_this_too(value=collections.defaultdict()):
|
||||
...
|
||||
|
||||
|
||||
def dont_forget_me(value=collections.deque()):
|
||||
...
|
||||
|
||||
|
||||
# N.B. we're also flagging the function call in the comprehension
|
||||
def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
|
||||
pass
|
||||
|
||||
|
||||
def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
|
||||
pass
|
||||
|
||||
|
||||
def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
|
||||
pass
|
||||
|
||||
|
||||
def kwonlyargs_mutable(*, value=[]):
|
||||
...
|
||||
|
||||
|
||||
# Recommended approach for mutable defaults
|
||||
def do_this_instead(value=None):
|
||||
if value is None:
|
||||
value = set()
|
||||
|
||||
|
||||
# B008
|
||||
# Flag function calls as default args (including if they are part of a sub-expression)
|
||||
def in_fact_all_calls_are_wrong(value=time.time()):
|
||||
...
|
||||
|
||||
|
||||
def f(when=dt.datetime.now() + dt.timedelta(days=7)):
|
||||
pass
|
||||
|
||||
|
||||
def can_even_catch_lambdas(a=(lambda x: x)()):
|
||||
...
|
||||
|
||||
|
||||
# Recommended approach for function calls as default args
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def do_this_instead_of_calls_in_defaults(logger=LOGGER):
|
||||
# That makes it more obvious that this one value is reused.
|
||||
...
|
||||
|
||||
|
||||
# Handle inf/infinity/nan special case
|
||||
def float_inf_okay(value=float("inf")):
|
||||
pass
|
||||
|
||||
|
||||
def float_infinity_okay(value=float("infinity")):
|
||||
pass
|
||||
|
||||
|
||||
def float_plus_infinity_okay(value=float("+infinity")):
|
||||
pass
|
||||
|
||||
|
||||
def float_minus_inf_okay(value=float("-inf")):
|
||||
pass
|
||||
|
||||
|
||||
def float_nan_okay(value=float("nan")):
|
||||
pass
|
||||
|
||||
|
||||
def float_minus_NaN_okay(value=float("-NaN")):
|
||||
pass
|
||||
|
||||
|
||||
def float_infinity_literal(value=float("1e999")):
|
||||
pass
|
||||
|
||||
|
||||
# But don't allow standard floats
|
||||
def float_int_is_wrong(value=float(3)):
|
||||
pass
|
||||
|
||||
|
||||
def float_str_not_inf_or_nan_is_wrong(value=float("3.14")):
|
||||
pass
|
||||
|
||||
|
||||
# B006 and B008
|
||||
# We should handle arbitrary nesting of these B008.
|
||||
def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
pass
|
||||
|
||||
|
||||
# Don't flag nested B006 since we can't guarantee that
|
||||
# it isn't made mutable by the outer operation.
|
||||
def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
|
||||
pass
|
||||
|
||||
|
||||
# B008-ception.
|
||||
def nested_b008(a=random.randint(0, dt.datetime.now().year)):
|
||||
pass
|
||||
|
||||
|
||||
# Ignore lambda contents since they are evaluated at call time.
|
||||
def foo(f=lambda x: print(x)):
|
||||
f(1)
|
||||
31
resources/test/fixtures/B007.py
vendored
Normal file
31
resources/test/fixtures/B007.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
for i in range(10):
|
||||
print(i)
|
||||
|
||||
print(i) # name no longer defined on Python 3; no warning yet
|
||||
|
||||
for i in range(10): # name not used within the loop; B007
|
||||
print(10)
|
||||
|
||||
print(i) # name no longer defined on Python 3; no warning yet
|
||||
|
||||
|
||||
for _ in range(10): # _ is okay for a throw-away variable
|
||||
print(10)
|
||||
|
||||
|
||||
for i in range(10):
|
||||
for j in range(10):
|
||||
for k in range(10): # k not used, i and j used transitively
|
||||
print(i + j)
|
||||
|
||||
|
||||
def strange_generator():
|
||||
for i in range(10):
|
||||
for j in range(10):
|
||||
for k in range(10):
|
||||
for l in range(10):
|
||||
yield i, (j, (k, l))
|
||||
|
||||
|
||||
for i, (j, (k, l)) in strange_generator(): # i, k not used
|
||||
print(j, l)
|
||||
10
resources/test/fixtures/B011.py
vendored
Normal file
10
resources/test/fixtures/B011.py
vendored
Normal 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"
|
||||
8
resources/test/fixtures/B013.py
vendored
Normal file
8
resources/test/fixtures/B013.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
try:
|
||||
pass
|
||||
except (ValueError,):
|
||||
pass
|
||||
except AttributeError:
|
||||
pass
|
||||
except (ImportError, TypeError):
|
||||
pass
|
||||
76
resources/test/fixtures/B014.py
vendored
Normal file
76
resources/test/fixtures/B014.py
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Should emit:
|
||||
B014 - on lines 11, 17, 28, 42, 49, 56, and 74.
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import re
|
||||
|
||||
try:
|
||||
pass
|
||||
except (Exception, TypeError):
|
||||
# TypeError is a subclass of Exception, so it doesn't add anything
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (OSError, OSError) as err:
|
||||
# Duplicate exception types are useless
|
||||
pass
|
||||
|
||||
|
||||
class MyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (MyError, MyError):
|
||||
# Detect duplicate non-builtin errors
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (MyError, Exception) as e:
|
||||
# Don't assume that we're all subclasses of Exception
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (MyError, BaseException) as e:
|
||||
# But we *can* assume that everything is a subclass of BaseException
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (re.error, re.error):
|
||||
# Duplicate exception types as attributes
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (IOError, EnvironmentError, OSError):
|
||||
# Detect if a primary exception and any its aliases are present.
|
||||
#
|
||||
# Since Python 3.3, IOError, EnvironmentError, WindowsError, mmap.error,
|
||||
# socket.error and select.error are aliases of OSError. See PEP 3151 for
|
||||
# more info.
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (MyException, NotImplemented):
|
||||
# NotImplemented is not an exception, let's not crash on it.
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (ValueError, binascii.Error):
|
||||
# binascii.Error is a subclass of ValueError.
|
||||
pass
|
||||
36
resources/test/fixtures/B017.py
vendored
Normal file
36
resources/test/fixtures/B017.py
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Should emit:
|
||||
B017 - on lines 20
|
||||
"""
|
||||
import asyncio
|
||||
import unittest
|
||||
|
||||
CONSTANT = True
|
||||
|
||||
|
||||
def something_else() -> None:
|
||||
for i in (1, 2, 3):
|
||||
print(i)
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
|
||||
class Foobar(unittest.TestCase):
|
||||
def evil_raises(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
raise Exception("Evil I say!")
|
||||
|
||||
def context_manager_raises(self) -> None:
|
||||
with self.assertRaises(Exception) as ex:
|
||||
raise Exception("Context manager is good")
|
||||
self.assertEqual("Context manager is good", str(ex.exception))
|
||||
|
||||
def regex_raises(self) -> None:
|
||||
with self.assertRaisesRegex(Exception, "Regex is good"):
|
||||
raise Exception("Regex is good")
|
||||
|
||||
def raises_with_absolute_reference(self):
|
||||
with self.assertRaises(asyncio.CancelledError):
|
||||
Foo()
|
||||
38
resources/test/fixtures/B025.py
vendored
Normal file
38
resources/test/fixtures/B025.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Should emit:
|
||||
B025 - on lines 15, 22, 31
|
||||
"""
|
||||
|
||||
import pickle
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except ValueError:
|
||||
a = 2
|
||||
finally:
|
||||
a = 3
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except ValueError:
|
||||
a = 2
|
||||
except ValueError:
|
||||
a = 2
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except pickle.PickleError:
|
||||
a = 2
|
||||
except ValueError:
|
||||
a = 2
|
||||
except pickle.PickleError:
|
||||
a = 2
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except (ValueError, TypeError):
|
||||
a = 2
|
||||
except ValueError:
|
||||
a = 2
|
||||
except (OSError, TypeError):
|
||||
a = 2
|
||||
3
resources/test/fixtures/C402.py
vendored
3
resources/test/fixtures/C402.py
vendored
@@ -1 +1,2 @@
|
||||
d = dict((x, x) for x in range(3))
|
||||
dict((x, x) for x in range(3))
|
||||
dict(((x, x) for x in range(3)), z=3)
|
||||
|
||||
3
resources/test/fixtures/C404.py
vendored
3
resources/test/fixtures/C404.py
vendored
@@ -1 +1,2 @@
|
||||
d = dict([(i, i) for i in range(3)])
|
||||
dict([(i, i) for i in range(3)])
|
||||
dict([(i, i) for i in range(3)], z=4)
|
||||
|
||||
3
resources/test/fixtures/C409.py
vendored
Normal file
3
resources/test/fixtures/C409.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
t1 = tuple([1, 2])
|
||||
t2 = tuple((1, 2))
|
||||
t3 = tuple([])
|
||||
4
resources/test/fixtures/C410.py
vendored
Normal file
4
resources/test/fixtures/C410.py
vendored
Normal 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
2
resources/test/fixtures/C411.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
x = [1, 2, 3]
|
||||
list([i for i in x])
|
||||
4
resources/test/fixtures/C413.py
vendored
Normal file
4
resources/test/fixtures/C413.py
vendored
Normal 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
14
resources/test/fixtures/C414.py
vendored
Normal 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
9
resources/test/fixtures/C415.py
vendored
Normal 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
6
resources/test/fixtures/C416.py
vendored
Normal 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
6
resources/test/fixtures/C417.py
vendored
Normal 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
534
resources/test/fixtures/D.py
vendored
Normal 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')
|
||||
@@ -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
5
resources/test/fixtures/F401_1.py
vendored
Normal 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
12
resources/test/fixtures/F401_2.py
vendored
Normal 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
14
resources/test/fixtures/F401_3.py
vendored
Normal 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
14
resources/test/fixtures/F401_4.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Test: referencing an import via TypeAlias (with future annotations and quotes)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
|
||||
CustomInt: TypeAlias = "np.int8 | np.int16"
|
||||
5
resources/test/fixtures/F401_5.py
vendored
Normal file
5
resources/test/fixtures/F401_5.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Test: removal of multi-segment and aliases imports."""
|
||||
from a.b import c
|
||||
from d.e import f as g
|
||||
import h.i
|
||||
import j.k as l
|
||||
1
resources/test/fixtures/F404.py
vendored
1
resources/test/fixtures/F404.py
vendored
@@ -1,4 +1,3 @@
|
||||
from __future__ import print_function
|
||||
"""Docstring"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
|
||||
9
resources/test/fixtures/F821.py
vendored
9
resources/test/fixtures/F821.py
vendored
@@ -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
|
||||
|
||||
3
resources/test/fixtures/M001.py
vendored
3
resources/test/fixtures/M001.py
vendored
@@ -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
34
resources/test/fixtures/N801.py
vendored
Normal 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
|
||||
41
resources/test/fixtures/N802.py
vendored
Normal file
41
resources/test/fixtures/N802.py
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
import unittest
|
||||
|
||||
|
||||
def Bad():
|
||||
pass
|
||||
|
||||
|
||||
def _Bad():
|
||||
pass
|
||||
|
||||
|
||||
def BAD():
|
||||
pass
|
||||
|
||||
|
||||
def BAD_FUNC():
|
||||
pass
|
||||
|
||||
|
||||
def good():
|
||||
pass
|
||||
|
||||
|
||||
def _good():
|
||||
pass
|
||||
|
||||
|
||||
def good_func():
|
||||
pass
|
||||
|
||||
|
||||
def tearDownModule():
|
||||
pass
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
return super().tearDown()
|
||||
|
||||
def testTest(self):
|
||||
assert True
|
||||
7
resources/test/fixtures/N803.py
vendored
Normal file
7
resources/test/fixtures/N803.py
vendored
Normal 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
19
resources/test/fixtures/N804.py
vendored
Normal 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
26
resources/test/fixtures/N805.py
vendored
Normal 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
|
||||
4
resources/test/fixtures/N806.py
vendored
Normal file
4
resources/test/fixtures/N806.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
def f():
|
||||
lower = 0
|
||||
Camel = 0
|
||||
CONSTANT = 0
|
||||
15
resources/test/fixtures/N807.py
vendored
Normal file
15
resources/test/fixtures/N807.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
def __bad__():
|
||||
pass
|
||||
|
||||
|
||||
def __good():
|
||||
pass
|
||||
|
||||
|
||||
def good__():
|
||||
pass
|
||||
|
||||
|
||||
class Class:
|
||||
def __good__(self):
|
||||
pass
|
||||
3
resources/test/fixtures/N811.py
vendored
Normal file
3
resources/test/fixtures/N811.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import mod.CONST as const
|
||||
from mod import CONSTANT as constant
|
||||
from mod import ANOTHER_CONSTANT as another_constant
|
||||
3
resources/test/fixtures/N812.py
vendored
Normal file
3
resources/test/fixtures/N812.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import modl.lowercase as Lower
|
||||
from mod import lowercase as Lowercase
|
||||
from mod import another_lowercase as AnotherLowercase
|
||||
3
resources/test/fixtures/N813.py
vendored
Normal file
3
resources/test/fixtures/N813.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import mod.Camel as camel
|
||||
from mod import CamelCase as camelcase
|
||||
from mod import AnotherCamelCase as another_camelcase
|
||||
3
resources/test/fixtures/N814.py
vendored
Normal file
3
resources/test/fixtures/N814.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import mod.Camel as CAMEL
|
||||
from mod import CamelCase as CAMELCASE
|
||||
from mod import AnotherCamelCase as ANOTHER_CAMELCASE
|
||||
6
resources/test/fixtures/N815.py
vendored
Normal file
6
resources/test/fixtures/N815.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
class C:
|
||||
lower = 0
|
||||
CONSTANT = 0
|
||||
mixedCase = 0
|
||||
_mixedCase = 0
|
||||
mixed_Case = 0
|
||||
5
resources/test/fixtures/N816.py
vendored
Normal file
5
resources/test/fixtures/N816.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
lower = 0
|
||||
CONSTANT = 0
|
||||
mixedCase = 0
|
||||
_mixedCase = 0
|
||||
mixed_Case = 0
|
||||
2
resources/test/fixtures/N817.py
vendored
Normal file
2
resources/test/fixtures/N817.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import mod.CaMel as CM
|
||||
from mod import CamelCase as CC
|
||||
10
resources/test/fixtures/N818.py
vendored
Normal file
10
resources/test/fixtures/N818.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AnotherError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class C(Exception):
|
||||
pass
|
||||
13
resources/test/fixtures/U005.py
vendored
13
resources/test/fixtures/U005.py
vendored
@@ -1,3 +1,10 @@
|
||||
self.assertEquals (1, 2)
|
||||
self.assertEquals(1, 2)
|
||||
self.assertEqual(3, 4)
|
||||
import unittest
|
||||
|
||||
|
||||
class Suite(unittest.TestCase):
|
||||
def test(self) -> None:
|
||||
self.assertEquals (1, 2)
|
||||
self.assertEquals(1, 2)
|
||||
self.assertEqual(3, 4)
|
||||
self.failUnlessAlmostEqual(1, 1.1)
|
||||
self.assertNotRegexpMatches("a", "b")
|
||||
|
||||
35
resources/test/fixtures/W605.py
vendored
Normal file
35
resources/test/fixtures/W605.py
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
#: W605:1:10
|
||||
regex = '\.png$'
|
||||
|
||||
#: W605:2:1
|
||||
regex = '''
|
||||
\.png$
|
||||
'''
|
||||
|
||||
#: W605:2:6
|
||||
f(
|
||||
'\_'
|
||||
)
|
||||
|
||||
#: W605:4:6
|
||||
"""
|
||||
multi-line
|
||||
literal
|
||||
with \_ somewhere
|
||||
in the middle
|
||||
"""
|
||||
|
||||
#: Okay
|
||||
regex = r'\.png$'
|
||||
regex = '\\.png$'
|
||||
regex = r'''
|
||||
\.png$
|
||||
'''
|
||||
regex = r'''
|
||||
\\.png$
|
||||
'''
|
||||
s = '\\'
|
||||
regex = '\w' # noqa
|
||||
regex = '''
|
||||
\w
|
||||
''' # noqa
|
||||
2
resources/test/fixtures/__init__.py
vendored
2
resources/test/fixtures/__init__.py
vendored
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
print(__path__)
|
||||
|
||||
__all__ = ["a", "b", "c"]
|
||||
|
||||
108
resources/test/fixtures/canonical_google_examples.py
vendored
Normal file
108
resources/test/fixtures/canonical_google_examples.py
vendored
Normal 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."""
|
||||
163
resources/test/fixtures/canonical_numpy_examples.py
vendored
Normal file
163
resources/test/fixtures/canonical_numpy_examples.py
vendored
Normal 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
|
||||
38
resources/test/fixtures/flake8_quotes/docstring_doubles.py
vendored
Normal file
38
resources/test/fixtures/flake8_quotes/docstring_doubles.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Double quotes multiline module docstring
|
||||
"""
|
||||
|
||||
"""
|
||||
this is not a docstring
|
||||
"""
|
||||
|
||||
l = []
|
||||
|
||||
class Cls:
|
||||
"""
|
||||
Double quotes multiline class docstring
|
||||
"""
|
||||
|
||||
"""
|
||||
this is not a docstring
|
||||
"""
|
||||
|
||||
# The colon in the list indexing below is an edge case for the docstring scanner
|
||||
def f(self, bar="""
|
||||
definitely not a docstring""",
|
||||
val=l[Cls():3]):
|
||||
"""
|
||||
Double quotes multiline function docstring
|
||||
"""
|
||||
|
||||
some_expression = 'hello world'
|
||||
|
||||
"""
|
||||
this is not a docstring
|
||||
"""
|
||||
|
||||
if l:
|
||||
"""
|
||||
Looks like a docstring, but in reality it isn't - only modules, classes and functions
|
||||
"""
|
||||
pass
|
||||
9
resources/test/fixtures/flake8_quotes/docstring_doubles_class.py
vendored
Normal file
9
resources/test/fixtures/flake8_quotes/docstring_doubles_class.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
class SingleLineDocstrings():
|
||||
""" Double quotes single line class docstring """
|
||||
""" Not a docstring """
|
||||
|
||||
def foo(self, bar="""not a docstring"""):
|
||||
""" Double quotes single line method docstring"""
|
||||
pass
|
||||
|
||||
class Nested(foo()[:]): """ inline docstring """; pass
|
||||
22
resources/test/fixtures/flake8_quotes/docstring_doubles_function.py
vendored
Normal file
22
resources/test/fixtures/flake8_quotes/docstring_doubles_function.py
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
def foo():
|
||||
"""function without params, single line docstring"""
|
||||
""" not a docstring"""
|
||||
return
|
||||
|
||||
|
||||
def foo2():
|
||||
"""
|
||||
function without params, multiline docstring
|
||||
"""
|
||||
""" not a docstring"""
|
||||
return
|
||||
|
||||
|
||||
def fun_with_params_no_docstring(a, b="""
|
||||
not a
|
||||
""" """docstring"""):
|
||||
pass
|
||||
|
||||
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
|
||||
""" not a docstring """):
|
||||
pass
|
||||
11
resources/test/fixtures/flake8_quotes/docstring_doubles_module_multiline.py
vendored
Normal file
11
resources/test/fixtures/flake8_quotes/docstring_doubles_module_multiline.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Double quotes multiline module docstring
|
||||
"""
|
||||
"""
|
||||
this is not a docstring
|
||||
"""
|
||||
def foo():
|
||||
pass
|
||||
"""
|
||||
this is not a docstring
|
||||
"""
|
||||
6
resources/test/fixtures/flake8_quotes/docstring_doubles_module_singleline.py
vendored
Normal file
6
resources/test/fixtures/flake8_quotes/docstring_doubles_module_singleline.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
""" Double quotes singleline module docstring """
|
||||
""" this is not a docstring """
|
||||
|
||||
def foo():
|
||||
pass
|
||||
""" this is not a docstring """
|
||||
40
resources/test/fixtures/flake8_quotes/docstring_singles.py
vendored
Normal file
40
resources/test/fixtures/flake8_quotes/docstring_singles.py
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
'''
|
||||
Single quotes multiline module docstring
|
||||
'''
|
||||
|
||||
'''
|
||||
this is not a docstring
|
||||
'''
|
||||
|
||||
l = []
|
||||
|
||||
class Cls(MakeKlass('''
|
||||
class params \t not a docstring
|
||||
''')):
|
||||
'''
|
||||
Single quotes multiline class docstring
|
||||
'''
|
||||
|
||||
'''
|
||||
this is not a docstring
|
||||
'''
|
||||
|
||||
# The colon in the list indexing below is an edge case for the docstring scanner
|
||||
def f(self, bar='''
|
||||
definitely not a docstring''',
|
||||
val=l[Cls():3]):
|
||||
'''
|
||||
Single quotes multiline function docstring
|
||||
'''
|
||||
|
||||
some_expression = 'hello world'
|
||||
|
||||
'''
|
||||
this is not a docstring
|
||||
'''
|
||||
|
||||
if l:
|
||||
'''
|
||||
Looks like a docstring, but in reality it isn't - only modules, classes and functions
|
||||
'''
|
||||
pass
|
||||
9
resources/test/fixtures/flake8_quotes/docstring_singles_class.py
vendored
Normal file
9
resources/test/fixtures/flake8_quotes/docstring_singles_class.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
class SingleLineDocstrings():
|
||||
''' Double quotes single line class docstring '''
|
||||
''' Not a docstring '''
|
||||
|
||||
def foo(self, bar='''not a docstring'''):
|
||||
''' Double quotes single line method docstring'''
|
||||
pass
|
||||
|
||||
class Nested(foo()[:]): ''' inline docstring '''; pass
|
||||
23
resources/test/fixtures/flake8_quotes/docstring_singles_function.py
vendored
Normal file
23
resources/test/fixtures/flake8_quotes/docstring_singles_function.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
def foo():
|
||||
'''function without params, single line docstring'''
|
||||
''' not a docstring'''
|
||||
return
|
||||
|
||||
|
||||
def foo2():
|
||||
'''
|
||||
function without params, multiline docstring
|
||||
'''
|
||||
''' not a docstring'''
|
||||
return
|
||||
|
||||
|
||||
def fun_with_params_no_docstring(a, b='''
|
||||
not a
|
||||
''' '''docstring'''):
|
||||
pass
|
||||
|
||||
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
|
||||
''' not a docstring '''):
|
||||
pass
|
||||
|
||||
11
resources/test/fixtures/flake8_quotes/docstring_singles_module_multiline.py
vendored
Normal file
11
resources/test/fixtures/flake8_quotes/docstring_singles_module_multiline.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
'''
|
||||
Double quotes multiline module docstring
|
||||
'''
|
||||
'''
|
||||
this is not a docstring
|
||||
'''
|
||||
def foo():
|
||||
pass
|
||||
'''
|
||||
this is not a docstring
|
||||
'''
|
||||
6
resources/test/fixtures/flake8_quotes/docstring_singles_module_singleline.py
vendored
Normal file
6
resources/test/fixtures/flake8_quotes/docstring_singles_module_singleline.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
''' Double quotes singleline module docstring '''
|
||||
''' this is not a docstring '''
|
||||
|
||||
def foo():
|
||||
pass
|
||||
''' this is not a docstring '''
|
||||
2
resources/test/fixtures/flake8_quotes/doubles.py
vendored
Normal file
2
resources/test/fixtures/flake8_quotes/doubles.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
this_should_be_linted = "double quote string"
|
||||
this_should_be_linted = u"double quote string"
|
||||
5
resources/test/fixtures/flake8_quotes/doubles_escaped.py
vendored
Normal file
5
resources/test/fixtures/flake8_quotes/doubles_escaped.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
this_should_raise_Q003 = 'This is a \'string\''
|
||||
this_is_fine = '"This" is a \'string\''
|
||||
this_is_fine = "This is a 'string'"
|
||||
this_is_fine = "\"This\" is a 'string'"
|
||||
this_is_fine = r'This is a \'string\''
|
||||
9
resources/test/fixtures/flake8_quotes/doubles_multiline_string.py
vendored
Normal file
9
resources/test/fixtures/flake8_quotes/doubles_multiline_string.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
s = """ This "should"
|
||||
be
|
||||
"linted" """
|
||||
|
||||
s = ''' This "should"
|
||||
"not" be
|
||||
"linted" '''
|
||||
|
||||
s = """'This should not be linted due to having would-be quadruple end quote'"""
|
||||
1
resources/test/fixtures/flake8_quotes/doubles_noqa.py
vendored
Normal file
1
resources/test/fixtures/flake8_quotes/doubles_noqa.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
this_should_not_be_linted = "double quote string" # noqa
|
||||
2
resources/test/fixtures/flake8_quotes/doubles_wrapped.py
vendored
Normal file
2
resources/test/fixtures/flake8_quotes/doubles_wrapped.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
s = 'double "quotes" wrapped in singles are ignored'
|
||||
s = "single 'quotes' wrapped in doubles are ignored"
|
||||
2
resources/test/fixtures/flake8_quotes/singles.py
vendored
Normal file
2
resources/test/fixtures/flake8_quotes/singles.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
this_should_be_linted = 'single quote string'
|
||||
this_should_be_linted = u'double quote string'
|
||||
5
resources/test/fixtures/flake8_quotes/singles_escaped.py
vendored
Normal file
5
resources/test/fixtures/flake8_quotes/singles_escaped.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
this_should_raise_Q003 = "This is a \"string\""
|
||||
this_is_fine = "'This' is a \"string\""
|
||||
this_is_fine = 'This is a "string"'
|
||||
this_is_fine = '\'This\' is a "string"'
|
||||
this_is_fine = r"This is a \"string\""
|
||||
9
resources/test/fixtures/flake8_quotes/singles_multiline_string.py
vendored
Normal file
9
resources/test/fixtures/flake8_quotes/singles_multiline_string.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
s = ''' This 'should'
|
||||
be
|
||||
'linted' '''
|
||||
|
||||
s = """ This 'should'
|
||||
'not' be
|
||||
'linted' """
|
||||
|
||||
s = '''"This should not be linted due to having would-be quadruple end quote"'''
|
||||
1
resources/test/fixtures/flake8_quotes/singles_noqa.py
vendored
Normal file
1
resources/test/fixtures/flake8_quotes/singles_noqa.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
this_should_not_be_linted = 'single quote string' # noqa
|
||||
2
resources/test/fixtures/flake8_quotes/singles_wrapped.py
vendored
Normal file
2
resources/test/fixtures/flake8_quotes/singles_wrapped.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
s = "single 'quotes' wrapped in doubles are ignored"
|
||||
s = 'double "quotes" wrapped in singles are ignored'
|
||||
31
resources/test/fixtures/pyproject.toml
vendored
31
resources/test/fixtures/pyproject.toml
vendored
@@ -5,3 +5,34 @@ extend-exclude = [
|
||||
"migrations",
|
||||
"directory/also_excluded.py",
|
||||
]
|
||||
per-file-ignores = [
|
||||
"__init__.py:F401",
|
||||
]
|
||||
|
||||
[tool.ruff.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
multiline-quotes = "double"
|
||||
docstring-quotes = "double"
|
||||
avoid-escape = true
|
||||
|
||||
[tool.ruff.pep8-naming]
|
||||
ignore-names = [
|
||||
"setUp",
|
||||
"tearDown",
|
||||
"setUpClass",
|
||||
"tearDownClass",
|
||||
"setUpModule",
|
||||
"tearDownModule",
|
||||
"asyncSetUp",
|
||||
"asyncTearDown",
|
||||
"setUpTestData",
|
||||
"failureException",
|
||||
"longMessage",
|
||||
"maxDiff",
|
||||
]
|
||||
classmethod-decorators = [
|
||||
"classmethod",
|
||||
]
|
||||
staticmethod-decorators = [
|
||||
"staticmethod",
|
||||
]
|
||||
|
||||
497
resources/test/fixtures/sections.py
vendored
Normal file
497
resources/test/fixtures/sections.py
vendored
Normal 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
19
scripts/.flake8
Normal 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
23
setup.py
Normal 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=[])
|
||||
1085
src/ast/checks.rs
1085
src/ast/checks.rs
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,42 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_ast::{Expr, ExprKind, StmtKind};
|
||||
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, StmtKind};
|
||||
|
||||
use crate::python::typing;
|
||||
|
||||
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
|
||||
fn compose_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
|
||||
match &expr.node {
|
||||
ExprKind::Call { func, .. } => {
|
||||
compose_call_path_inner(func, parts);
|
||||
}
|
||||
ExprKind::Attribute { value, attr, .. } => {
|
||||
compose_call_path_inner(value, parts);
|
||||
parts.push(attr);
|
||||
}
|
||||
ExprKind::Name { id, .. } => {
|
||||
parts.push(id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose_call_path(expr: &Expr) -> Option<String> {
|
||||
let mut segments = vec![];
|
||||
compose_call_path_inner(expr, &mut segments);
|
||||
if segments.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(segments.join("."))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
|
||||
match &expr.node {
|
||||
ExprKind::Attribute { attr, .. } => target == attr,
|
||||
ExprKind::Name { id, .. } => target == id,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SubscriptKind {
|
||||
AnnotatedSubscript,
|
||||
@@ -35,6 +67,8 @@ pub fn match_annotated_subscript(expr: &Expr) -> Option<SubscriptKind> {
|
||||
}
|
||||
}
|
||||
|
||||
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
|
||||
|
||||
pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
|
||||
// Check whether it's an assignment to a dunder, with or without a type annotation.
|
||||
// This is what pycodestyle (as of 2.9.1) does.
|
||||
@@ -65,10 +99,47 @@ pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
|
||||
match &expr.node {
|
||||
ExprKind::Attribute { attr, .. } => target == attr,
|
||||
ExprKind::Name { id, .. } => target == id,
|
||||
_ => false,
|
||||
/// Extract the names of all handled exceptions.
|
||||
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<String> {
|
||||
let mut handler_names = vec![];
|
||||
for handler in handlers {
|
||||
match &handler.node {
|
||||
ExcepthandlerKind::ExceptHandler { type_, .. } => {
|
||||
if let Some(type_) = type_ {
|
||||
if let ExprKind::Tuple { elts, .. } = &type_.node {
|
||||
for type_ in elts {
|
||||
if let Some(name) = compose_call_path(type_) {
|
||||
handler_names.push(name);
|
||||
}
|
||||
}
|
||||
} else if let Some(name) = compose_call_path(type_) {
|
||||
handler_names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handler_names
|
||||
}
|
||||
|
||||
/// Returns `true` if a call is an argumented `super` invocation.
|
||||
pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
|
||||
// Check: is this a `super` call?
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
id == "super" && !args.is_empty()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a location within a file (relative to `base`) to an absolute position.
|
||||
pub fn to_absolute(relative: &Location, base: &Location) -> Location {
|
||||
if relative.row() == 1 {
|
||||
Location::new(
|
||||
relative.row() + base.row() - 1,
|
||||
relative.column() + base.column(),
|
||||
)
|
||||
} else {
|
||||
Location::new(relative.row() + base.row() - 1, relative.column())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod checks;
|
||||
pub mod helpers;
|
||||
pub mod operations;
|
||||
pub mod relocate;
|
||||
@@ -1,6 +1,6 @@
|
||||
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
|
||||
use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
|
||||
|
||||
use crate::ast::types::{BindingKind, Range, Scope};
|
||||
use crate::ast::types::{BindingKind, Scope};
|
||||
|
||||
/// Extract the names bound to a given __all__ assignment.
|
||||
pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
|
||||
@@ -117,49 +117,3 @@ pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Struct used to efficiently slice source code at (row, column) Locations.
|
||||
pub struct SourceCodeLocator<'a> {
|
||||
content: &'a str,
|
||||
offsets: Vec<usize>,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
impl<'a> SourceCodeLocator<'a> {
|
||||
pub fn new(content: &'a str) -> Self {
|
||||
SourceCodeLocator {
|
||||
content,
|
||||
offsets: vec![],
|
||||
initialized: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slice_source_code_at(&mut self, location: &Location) -> &'a str {
|
||||
if !self.initialized {
|
||||
let mut offset = 0;
|
||||
for i in self.content.lines() {
|
||||
self.offsets.push(offset);
|
||||
offset += i.len();
|
||||
offset += 1;
|
||||
}
|
||||
self.initialized = true;
|
||||
}
|
||||
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.content[start..end]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod fixer;
|
||||
pub mod fixes;
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::checks::{Check, Fix};
|
||||
use crate::autofix::Fix;
|
||||
use crate::autofix::Patch;
|
||||
use crate::checks::Check;
|
||||
|
||||
#[derive(Hash)]
|
||||
pub enum Mode {
|
||||
@@ -14,6 +14,17 @@ pub enum Mode {
|
||||
None,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// Return `true` if a patch should be generated under the given `Mode`.
|
||||
pub fn patch(&self) -> bool {
|
||||
match &self {
|
||||
Mode::Generate => true,
|
||||
Mode::Apply => true,
|
||||
Mode::None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Mode {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
@@ -24,51 +35,58 @@ 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_str(&lines[last_pos.row() - 1][last_pos.column()..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..fix.location.row() - 1] {
|
||||
for line in &lines[last_pos.row()..fix.patch.location.row() - 1] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(&lines[fix.location.row() - 1][..fix.location.column() - 1]);
|
||||
output.push_str(&fix.content);
|
||||
output.push_str(&lines[fix.patch.location.row() - 1][..fix.patch.location.column()]);
|
||||
output.push_str(&fix.patch.content);
|
||||
} else {
|
||||
output.push_str(
|
||||
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.location.column() - 1],
|
||||
&lines[last_pos.row() - 1][last_pos.column()..fix.patch.location.column()],
|
||||
);
|
||||
output.push_str(&fix.content);
|
||||
output.push_str(&fix.patch.content);
|
||||
}
|
||||
last_pos = fix.patch.end_location;
|
||||
|
||||
last_pos = fix.end_location;
|
||||
applied.insert(&fix.patch);
|
||||
fix.applied = true;
|
||||
}
|
||||
|
||||
@@ -76,7 +94,7 @@ fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) ->
|
||||
&& (last_pos.row() - 1) < lines.len()
|
||||
&& (last_pos.row() > 0 || last_pos.column() > 0)
|
||||
{
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column()..]);
|
||||
output.push('\n');
|
||||
}
|
||||
if last_pos.row() < lines.len() {
|
||||
@@ -95,7 +113,8 @@ mod tests {
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::fixer::apply_fixes;
|
||||
use crate::checks::Fix;
|
||||
use crate::autofix::Fix;
|
||||
use crate::autofix::Patch;
|
||||
|
||||
#[test]
|
||||
fn empty_file() -> Result<()> {
|
||||
@@ -111,9 +130,11 @@ mod tests {
|
||||
#[test]
|
||||
fn apply_single_replacement() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "Bar".to_string(),
|
||||
location: Location::new(1, 9),
|
||||
end_location: Location::new(1, 15),
|
||||
patch: Patch {
|
||||
content: "Bar".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 14),
|
||||
},
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
@@ -135,9 +156,11 @@ mod tests {
|
||||
#[test]
|
||||
fn apply_single_removal() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 16),
|
||||
patch: Patch {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 15),
|
||||
},
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
@@ -160,15 +183,19 @@ mod tests {
|
||||
fn apply_double_removal() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 17),
|
||||
patch: Patch {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 16),
|
||||
},
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 17),
|
||||
end_location: Location::new(1, 24),
|
||||
patch: Patch {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 16),
|
||||
end_location: Location::new(1, 23),
|
||||
},
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
@@ -192,15 +219,19 @@ mod tests {
|
||||
fn ignore_overlapping_fixes() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 16),
|
||||
patch: Patch {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 15),
|
||||
},
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "ignored".to_string(),
|
||||
location: Location::new(1, 10),
|
||||
end_location: Location::new(1, 12),
|
||||
patch: Patch {
|
||||
content: "ignored".to_string(),
|
||||
location: Location::new(1, 9),
|
||||
end_location: Location::new(1, 11),
|
||||
},
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
89
src/autofix/helpers.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use rustpython_parser::ast::{ExcepthandlerKind, Location, Stmt, StmtKind};
|
||||
|
||||
use crate::autofix::Fix;
|
||||
|
||||
/// Determine if a body contains only a single statement, taking into account deleted.
|
||||
fn has_single_child(body: &[Stmt], deleted: &[&Stmt]) -> bool {
|
||||
body.iter().filter(|child| !deleted.contains(child)).count() == 1
|
||||
}
|
||||
|
||||
/// Determine if a child is the only statement in its body.
|
||||
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
|
||||
match &parent.node {
|
||||
StmtKind::FunctionDef { body, .. }
|
||||
| StmtKind::AsyncFunctionDef { body, .. }
|
||||
| StmtKind::ClassDef { body, .. }
|
||||
| StmtKind::With { body, .. }
|
||||
| StmtKind::AsyncWith { body, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
StmtKind::For { body, orelse, .. }
|
||||
| StmtKind::AsyncFor { body, orelse, .. }
|
||||
| StmtKind::While { body, orelse, .. }
|
||||
| StmtKind::If { body, orelse, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else if orelse.iter().contains(child) {
|
||||
Ok(has_single_child(orelse, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
StmtKind::Try {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
} => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else if orelse.iter().contains(child) {
|
||||
Ok(has_single_child(orelse, deleted))
|
||||
} else if finalbody.iter().contains(child) {
|
||||
Ok(has_single_child(finalbody, deleted))
|
||||
} else if let Some(body) = handlers.iter().find_map(|handler| match &handler.node {
|
||||
ExcepthandlerKind::ExceptHandler { body, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Some(body)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unable to find child in parent body.")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
|
||||
if parent
|
||||
.map(|parent| is_lone_child(stmt, parent, deleted))
|
||||
.map_or(Ok(None), |v| v.map(Some))?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// If removing this node would lead to an invalid syntax tree, replace
|
||||
// it with a `pass`.
|
||||
Ok(Fix::replacement(
|
||||
"pass".to_string(),
|
||||
stmt.location,
|
||||
stmt.end_location.unwrap(),
|
||||
))
|
||||
} else {
|
||||
// Otherwise, nuke the entire line.
|
||||
// TODO(charlie): This logic assumes that there are no multi-statement physical lines.
|
||||
Ok(Fix::deletion(
|
||||
Location::new(stmt.location.row(), 0),
|
||||
Location::new(stmt.end_location.unwrap().row() + 1, 0),
|
||||
))
|
||||
}
|
||||
}
|
||||
53
src/autofix/mod.rs
Normal file
53
src/autofix/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/cache.rs
10
src/cache.rs
@@ -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),
|
||||
|
||||
1154
src/check_ast.rs
1154
src/check_ast.rs
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,13 @@
|
||||
//! Lint rules based on checking raw physical lines.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode, CheckKind, Fix};
|
||||
use crate::autofix::Fix;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::noqa;
|
||||
use crate::noqa::Directive;
|
||||
use crate::settings::Settings;
|
||||
@@ -91,8 +94,8 @@ pub fn check_lines(
|
||||
let check = Check::new(
|
||||
CheckKind::LineTooLong(line_length, settings.line_length),
|
||||
Range {
|
||||
location: Location::new(lineno + 1, 1),
|
||||
end_location: Location::new(lineno + 1, line_length + 1),
|
||||
location: Location::new(lineno + 1, 0),
|
||||
end_location: Location::new(lineno + 1, line_length),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -161,20 +164,15 @@ pub fn check_lines(
|
||||
let mut check = Check::new(
|
||||
CheckKind::UnusedNOQA(None),
|
||||
Range {
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(row + 1, end + 1),
|
||||
location: Location::new(row + 1, start),
|
||||
end_location: Location::new(row + 1, end),
|
||||
},
|
||||
);
|
||||
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
check.amend(Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(
|
||||
row + 1,
|
||||
lines[row].chars().count() + 1,
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
if autofix.patch() {
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(row + 1, start),
|
||||
Location::new(row + 1, lines[row].chars().count()),
|
||||
));
|
||||
}
|
||||
line_checks.push(check);
|
||||
}
|
||||
@@ -184,41 +182,32 @@ pub fn check_lines(
|
||||
let mut valid_codes = vec![];
|
||||
for code in codes {
|
||||
if !matches.contains(&code) {
|
||||
invalid_codes.push(code);
|
||||
invalid_codes.push(code.to_string());
|
||||
} else {
|
||||
valid_codes.push(code);
|
||||
valid_codes.push(code.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if !invalid_codes.is_empty() {
|
||||
let mut check = Check::new(
|
||||
CheckKind::UnusedNOQA(Some(invalid_codes.join(", "))),
|
||||
CheckKind::UnusedNOQA(Some(invalid_codes)),
|
||||
Range {
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(row + 1, end + 1),
|
||||
location: Location::new(row + 1, start),
|
||||
end_location: Location::new(row + 1, end),
|
||||
},
|
||||
);
|
||||
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if autofix.patch() {
|
||||
if valid_codes.is_empty() {
|
||||
check.amend(Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(
|
||||
row + 1,
|
||||
lines[row].chars().count() + 1,
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(row + 1, start),
|
||||
Location::new(row + 1, lines[row].chars().count()),
|
||||
));
|
||||
} else {
|
||||
check.amend(Fix {
|
||||
content: format!(" # noqa: {}", valid_codes.join(", ")),
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(
|
||||
row + 1,
|
||||
lines[row].chars().count() + 1,
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
check.amend(Fix::replacement(
|
||||
format!(" # noqa: {}", valid_codes.join(", ")),
|
||||
Location::new(row + 1, start),
|
||||
Location::new(row + 1, lines[row].chars().count()),
|
||||
));
|
||||
}
|
||||
}
|
||||
line_checks.push(check);
|
||||
@@ -240,7 +229,7 @@ pub fn check_lines(
|
||||
mod tests {
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
use crate::settings;
|
||||
use crate::settings::Settings;
|
||||
|
||||
use super::check_lines;
|
||||
|
||||
@@ -254,9 +243,9 @@ mod tests {
|
||||
&mut checks,
|
||||
line,
|
||||
&noqa_line_for,
|
||||
&settings::Settings {
|
||||
&Settings {
|
||||
line_length,
|
||||
..settings::Settings::for_rule(CheckCode::E501)
|
||||
..Settings::for_rule(CheckCode::E501)
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
);
|
||||
|
||||
51
src/check_tokens.rs
Normal file
51
src/check_tokens.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! Lint rules based on token traversal.
|
||||
|
||||
use rustpython_parser::lexer::{LexResult, Tok};
|
||||
|
||||
use crate::checks::{Check, CheckCode};
|
||||
use crate::flake8_quotes::docstring_detection::StateMachine;
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
use crate::{flake8_quotes, pycodestyle, Settings};
|
||||
|
||||
pub fn check_tokens(
|
||||
checks: &mut Vec<Check>,
|
||||
locator: &SourceCodeLocator,
|
||||
tokens: &[LexResult],
|
||||
settings: &Settings,
|
||||
) {
|
||||
let enforce_invalid_escape_sequence = settings.enabled.contains(&CheckCode::W605);
|
||||
let enforce_quotes = settings.enabled.contains(&CheckCode::Q000)
|
||||
| settings.enabled.contains(&CheckCode::Q001)
|
||||
| settings.enabled.contains(&CheckCode::Q002)
|
||||
| settings.enabled.contains(&CheckCode::Q003);
|
||||
|
||||
let mut state_machine = StateMachine::new();
|
||||
for (start, tok, end) in tokens.iter().flatten() {
|
||||
// W605
|
||||
if enforce_invalid_escape_sequence {
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
checks.extend(pycodestyle::checks::invalid_escape_sequence(
|
||||
locator, start, end,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// flake8-quotes
|
||||
if enforce_quotes {
|
||||
let is_docstring = state_machine.consume(tok);
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
if let Some(check) = flake8_quotes::checks::quotes(
|
||||
locator,
|
||||
start,
|
||||
end,
|
||||
is_docstring,
|
||||
&settings.flake8_quotes,
|
||||
) {
|
||||
if settings.enabled.contains(check.kind.code()) {
|
||||
checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1044
src/checks.rs
1044
src/checks.rs
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user