Compare commits

...

40 Commits

Author SHA1 Message Date
Charlie Marsh
685d9ab848 Bump version to 0.0.236 2023-01-26 18:47:00 -05:00
Charlie Marsh
093f9156e1 Wrap return-bool-condition-directly fixes in bool() (#2240) 2023-01-26 18:22:34 -05:00
Charlie Marsh
615e62ae24 Clarify E-category rule support (#2239) 2023-01-26 18:12:28 -05:00
Charlie Marsh
76a0c45773 Track type-checking blocks during tree traversal (#2238) 2023-01-26 18:09:28 -05:00
Charlie Marsh
3ec46f0936 Allow pytest in shebang (#2237) 2023-01-26 17:32:23 -05:00
Charlie Marsh
a6ec2eb044 Avoid removing trailing comments on pass statements (#2235)
This isn't super consistent with some other rules, but... if you have a lone body, with a `pass`, followed by a comment, it's probably surprising if it gets removed. Let's retain the comment.

Closes #2231.
2023-01-26 17:29:13 -05:00
Simon Brugman
9d3a5530af refactor: move violations to linters (#2234) 2023-01-26 17:28:14 -05:00
Simon Brugman
8766e6a666 docs(readme): add featuretools (#2236) 2023-01-26 17:24:45 -05:00
Charlie Marsh
b08367b5a8 Avoid flagging blind exceptions with valid logging (#2232) 2023-01-26 17:05:01 -05:00
Charlie Marsh
98a8330124 Add stylist settings to all LibCST invocations (#2225) 2023-01-26 16:59:33 -05:00
Charlie Marsh
4d52ea87ef Implement exempt-modules setting from flake8-type-checking (#2230) 2023-01-26 16:55:32 -05:00
Charlie Marsh
291239b9f1 Fix range for try-consider-else (#2228) 2023-01-26 16:36:18 -05:00
Charlie Marsh
224334b6d1 Avoid erroneous class autofixes in indented blocks (#2226) 2023-01-26 16:24:21 -05:00
Charlie Marsh
0cab3f8437 Preserve indentation when fixing via LibCST (#2223) 2023-01-26 16:09:35 -05:00
Charlie Marsh
5f8810e987 Add strictness setting for flake8-typing-imports (#2221) 2023-01-26 16:04:21 -05:00
Charlie Marsh
f15c562a1c Remove unused overridden property (#2217) 2023-01-26 14:46:46 -05:00
Martin Fischer
4f3b63edd4 fix: --explain reporting the wrong linter
Fixes a regression introduced in 4e4643aa5d.

We want the longest prefixes to be checked first so we of course
have to reverse the sorting when sorting by prefix length.

Fixes #2210.
2023-01-26 13:53:35 -05:00
Simon Brugman
bab8691132 chore: fix script indent (#2213) 2023-01-26 13:53:22 -05:00
Charlie Marsh
50c85fd192 Add a fixable and unfixable example to the docs (#2211) 2023-01-26 13:23:21 -05:00
Martin Fischer
23819ae338 Group options in --help output and sort them by importance
`ruff --help` previously listed 37 options in no particular order
(with niche options like --isolated being listed before before essential
options such as --select).  This commit remedies that and additionally
groups the options by making use of the Clap help_heading feature.

Note that while the source code has previously also referred to
--add-noqa, --show-settings, and --show-files as "subcommands"
this commit intentionally does not list them under the new
Subcommands section since contrary to --explain and --clean
combining them with most of the other options makes sense.
2023-01-26 13:06:29 -05:00
Martin Fischer
4bf2879067 refactor: Move add_noqa if branch up 2023-01-26 13:06:29 -05:00
Martin Fischer
b359f3a9ff refactor: Get rid of ruff::resolver::FileDiscovery 2023-01-26 13:06:29 -05:00
Martin Fischer
73d0461d55 refactor: Check required_version in Settings::from_configuration
The idiomatic way in Rust is to make invalid types unrepresentable
instead of paranoidly calling a validate method everywhere.
2023-01-26 13:06:29 -05:00
Martin Fischer
55bb36fb8b refactor: Introduce LogLevelArgs 2023-01-26 13:06:29 -05:00
Martin Fischer
cebea16fe4 refactor: Move Args::partition call after panic::set_hook 2023-01-26 13:06:29 -05:00
Charlie Marsh
f7be192f8b Alphabetize Flake8 plugins in the README (#2209) 2023-01-26 13:05:30 -05:00
Edgar R. M
e88275280b Implement some rules from flake8-logging-format (#2150) 2023-01-26 12:58:10 -05:00
Samuel Cormier-Iijima
7370a27c09 Don't flag B009/B010 if identifiers would be mangled (#2204) 2023-01-26 12:56:18 -05:00
jvstme
0ad6b8224d Fix typo in src option docs (#2201) 2023-01-26 12:23:09 -05:00
Henry Schreiner
f3aa409d9a docs(readme): add pypa's build (#2200) 2023-01-26 12:18:04 -05:00
Charlie Marsh
adb5c5b150 Fix respect_gitignore reference (#2196) 2023-01-26 09:53:17 -05:00
Martin Fischer
b69b6a7ec8 refactor: Move comments before if conditions 2023-01-25 22:08:35 -05:00
Martin Fischer
a7d2def9cd refactor: Move ruff_cli::resolve to own module 2023-01-25 22:08:35 -05:00
Martin Fischer
d9ead4e6df refactor: Rename CLI arg structs from Cli to Args
Technically the command-line interface (CLI) encompasses both input and
output, so naming the input structs 'Args' is more accurate than 'Cli'.
2023-01-25 22:08:35 -05:00
Charlie Marsh
b346f74915 Run cargo update (#2185) 2023-01-25 21:32:44 -05:00
Eric Roberts
708295f4c9 Move violations for pycodestyle rules to rules files (#2138) 2023-01-25 20:11:36 -05:00
Charlie Marsh
4190f1045e Remove manual newline from autofix helpers (#2184) 2023-01-25 19:53:26 -05:00
Florian Stasse
353857e2a5 Implement TRY400 (#2115) 2023-01-25 19:42:19 -05:00
Denis Gavrilyuk
55b43c8ea7 feat: implement TRY002 and TRY003 (#2135) 2023-01-25 19:22:43 -05:00
Anders Kaseorg
6036d1bbe2 flake8_executable: Only match shebang at beginning of line (#2183)
The Python implementation uses `re.match` for this, which only matches
at the beginning of a line.

https://github.com/xuhdev/flake8-executable/blob/v2.1.3/flake8_executable/__init__.py#L124

We could use `Regex::captures_read_at`, but that’s a more complicated
API; it’s easier to anchor the regex with `^`.

Signed-off-by: Anders Kaseorg <andersk@mit.edu>

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-01-25 18:57:09 -05:00
159 changed files with 3877 additions and 1702 deletions

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.235
rev: v0.0.236
hooks:
- id: ruff
args: [--fix]

253
Cargo.lock generated
View File

@@ -82,9 +82,9 @@ dependencies = [
[[package]]
name = "assert_cmd"
version = "2.0.7"
version = "2.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa3d466004a8b4cb1bc34044240a2fd29d17607e2e3bd613eb44fd48e8100da3"
checksum = "9834fcc22e0874394a010230586367d4a3e9f11b560f469262678547e1d2575e"
dependencies = [
"bstr 1.1.0",
"doc-comment",
@@ -181,9 +181,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.11.1"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "cachedir"
@@ -236,12 +236,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "ciborium"
version = "0.2.0"
@@ -283,13 +277,13 @@ dependencies = [
[[package]]
name = "clap"
version = "4.0.32"
version = "4.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39"
checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76"
dependencies = [
"bitflags",
"clap_derive",
"clap_lex 0.3.0",
"clap_lex 0.3.1",
"is-terminal",
"once_cell",
"strsim",
@@ -298,11 +292,11 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.0.7"
version = "4.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10861370d2ba66b0f5989f83ebf35db6421713fd92351790e7fdd6c36774c56b"
checksum = "3d6540eedc41f8a5a76cf3d8d458057dcdf817be4158a55b5f861f7a5483de75"
dependencies = [
"clap 4.0.32",
"clap 4.1.4",
]
[[package]]
@@ -311,26 +305,26 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4160b4a4f72ef58bd766bad27c09e6ef1cc9d82a22f6a0f55d152985a4a48e31"
dependencies = [
"clap 4.0.32",
"clap 4.1.4",
"clap_complete",
"clap_complete_fig",
]
[[package]]
name = "clap_complete_fig"
version = "4.0.2"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46b30e010e669cd021e5004f3be26cff6b7c08d2a8a0d65b48d43a8cc0efd6c3"
checksum = "cf0c76d8fcf782a1102ccfcd10ca8246e7fdd609c1cd6deddbb96cb638e9bb5c"
dependencies = [
"clap 4.0.32",
"clap 4.1.4",
"clap_complete",
]
[[package]]
name = "clap_derive"
version = "4.0.21"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8"
dependencies = [
"heck",
"proc-macro-error",
@@ -350,9 +344,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade"
dependencies = [
"os_str_bytes",
]
@@ -399,15 +393,14 @@ checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a"
[[package]]
name = "console"
version = "0.15.2"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c"
checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"terminal_size",
"winapi",
"windows-sys",
]
[[package]]
@@ -551,9 +544,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.85"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd"
checksum = "b61a7545f753a88bcbe0a70de1fcc0221e10bfc752f576754fa91e663db1622e"
dependencies = [
"cc",
"cxxbridge-flags",
@@ -563,9 +556,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.85"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0"
checksum = "f464457d494b5ed6905c63b0c4704842aba319084a0a3561cdc1359536b53200"
dependencies = [
"cc",
"codespan-reporting",
@@ -578,15 +571,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.85"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59"
checksum = "43c7119ce3a3701ed81aca8410b9acf6fc399d2629d057b87e2efa4e63a3aaea"
[[package]]
name = "cxxbridge-macro"
version = "1.0.85"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6"
checksum = "65e07508b90551e610910fa648a1878991d367064997a596135b86df30daf07e"
dependencies = [
"proc-macro2",
"quote",
@@ -757,10 +750,10 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.235"
version = "0.0.236"
dependencies = [
"anyhow",
"clap 4.0.32",
"clap 4.1.4",
"colored",
"configparser",
"once_cell",
@@ -771,7 +764,7 @@ dependencies = [
"serde_json",
"strum",
"strum_macros",
"toml 0.6.0",
"toml",
]
[[package]]
@@ -833,18 +826,18 @@ dependencies = [
[[package]]
name = "glob"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
version = "0.4.9"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
dependencies = [
"aho-corasick",
"bstr 0.2.17",
"bstr 1.1.0",
"fnv",
"log",
"regex",
@@ -931,11 +924,10 @@ dependencies = [
[[package]]
name = "ignore"
version = "0.4.18"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
dependencies = [
"crossbeam-utils",
"globset",
"lazy_static",
"log",
@@ -989,9 +981,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.23.0"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48b08a091dfe5b09a6a9688c468fdd5b4396e92ce09e2eb932f0884b02788a4"
checksum = "f6f0f08b46e4379744de2ab67aa8f7de3ffd1da3e275adc41fcc82053ede46ff"
dependencies = [
"console",
"lazy_static",
@@ -1014,9 +1006,9 @@ dependencies = [
[[package]]
name = "io-lifetimes"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
dependencies = [
"libc",
"windows-sys",
@@ -1242,9 +1234,9 @@ dependencies = [
[[package]]
name = "matches"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
@@ -1302,9 +1294,9 @@ checksum = "d906846a98739ed9d73d66e62c2641eef8321f1734b7a1156ab045a0248fb2b3"
[[package]]
name = "nix"
version = "0.26.1"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags",
"cfg-if",
@@ -1369,9 +1361,9 @@ dependencies = [
[[package]]
name = "num-complex"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19"
checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d"
dependencies = [
"num-traits",
"serde",
@@ -1408,18 +1400,18 @@ dependencies = [
[[package]]
name = "num_enum"
version = "0.5.7"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9"
checksum = "8d829733185c1ca374f17e52b762f24f535ec625d2cc1f070e34c8a9068f341b"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.5.7"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
checksum = "2be1598bf1c313dcdd12092e3f1920f463462525a21b7b4e11b4168353d0123e"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -1457,9 +1449,9 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.5"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf"
dependencies = [
"cfg-if",
"libc",
@@ -1527,9 +1519,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pest"
version = "2.5.3"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a"
checksum = "4ab62d2fa33726dbe6321cc97ef96d8cde531e3eeaf858a058de53a8a6d40d8f"
dependencies = [
"thiserror",
"ucd-trie",
@@ -1537,9 +1529,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.5.3"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241cda393b0cdd65e62e07e12454f1f25d57017dcc514b1514cd3c4645e3a0a6"
checksum = "8bf026e2d0581559db66d837fe5242320f525d85c76283c61f4d51a1238d65ea"
dependencies = [
"pest",
"pest_generator",
@@ -1547,9 +1539,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.5.3"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c"
checksum = "2b27bd18aa01d91c8ed2b61ea23406a676b42d82609c6e2581fba42f0c15f17f"
dependencies = [
"pest",
"pest_meta",
@@ -1560,9 +1552,9 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.5.3"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef4f1332a8d4678b41966bb4cc1d0676880e84183a1ecc3f4b69f03e99c7a51"
checksum = "9f02b677c1859756359fc9983c2e56a0237f18624a3789528804406b7e915e5d"
dependencies = [
"once_cell",
"pest",
@@ -1703,9 +1695,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "2.1.4"
version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f54fc5dc63ed3bbf19494623db4f3af16842c0d975818e469022d09e53f0aa05"
checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd"
dependencies = [
"difflib",
"itertools",
@@ -1730,13 +1722,12 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "1.2.1"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9"
checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34"
dependencies = [
"once_cell",
"thiserror",
"toml 0.5.11",
"toml_edit",
]
[[package]]
@@ -1765,9 +1756,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.49"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
dependencies = [
"unicode-ident",
]
@@ -1852,9 +1843,9 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.10.1"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3"
checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
@@ -1884,9 +1875,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.7.0"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
dependencies = [
"aho-corasick",
"memchr",
@@ -1931,13 +1922,13 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.235"
version = "0.0.236"
dependencies = [
"anyhow",
"bitflags",
"cfg-if",
"chrono",
"clap 4.0.32",
"clap 4.1.4",
"colored",
"console_error_panic_hook",
"console_log",
@@ -1979,14 +1970,14 @@ dependencies = [
"textwrap",
"thiserror",
"titlecase",
"toml 0.6.0",
"toml",
"wasm-bindgen",
"wasm-bindgen-test",
]
[[package]]
name = "ruff_cli"
version = "0.0.235"
version = "0.0.236"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1995,7 +1986,7 @@ dependencies = [
"bincode",
"cachedir",
"chrono",
"clap 4.0.32",
"clap 4.1.4",
"clap_complete_command",
"clearscreen",
"colored",
@@ -2023,10 +2014,10 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.235"
version = "0.0.236"
dependencies = [
"anyhow",
"clap 4.0.32",
"clap 4.1.4",
"itertools",
"libcst",
"once_cell",
@@ -2044,7 +2035,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.235"
version = "0.0.236"
dependencies = [
"once_cell",
"proc-macro2",
@@ -2071,9 +2062,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.36.5"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588"
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
dependencies = [
"bitflags",
"errno",
@@ -2085,9 +2076,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.20.7"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c"
checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
dependencies = [
"log",
"ring",
@@ -2253,9 +2244,9 @@ checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
[[package]]
name = "serde"
version = "1.0.151"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [
"serde_derive",
]
@@ -2273,9 +2264,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.151"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
@@ -2448,23 +2439,13 @@ dependencies = [
[[package]]
name = "termcolor"
version = "1.1.3"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "terminfo"
version = "0.7.5"
@@ -2602,15 +2583,6 @@ dependencies = [
"regex",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.6.0"
@@ -2733,9 +2705,9 @@ dependencies = [
[[package]]
name = "unicode-bidi"
version = "0.3.8"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
[[package]]
name = "unicode-ident"
@@ -2801,12 +2773,11 @@ dependencies = [
[[package]]
name = "ureq"
version = "2.5.0"
version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f"
checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d"
dependencies = [
"base64",
"chunked_transfer",
"flate2",
"log",
"once_cell",
@@ -3000,9 +2971,9 @@ dependencies = [
[[package]]
name = "which"
version = "4.3.0"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b"
checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
dependencies = [
"either",
"libc",
@@ -3063,45 +3034,45 @@ dependencies = [
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "yaml-rust"

View File

@@ -8,7 +8,7 @@ default-members = [".", "ruff_cli"]
[package]
name = "ruff"
version = "0.0.235"
version = "0.0.236"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -46,7 +46,7 @@ num-traits = "0.2.15"
once_cell = { version = "1.16.0" }
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
regex = { version = "1.6.0" }
ruff_macros = { version = "0.0.235", path = "ruff_macros" }
ruff_macros = { version = "0.0.236", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }

205
LICENSE
View File

@@ -800,3 +800,208 @@ are:
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- flake8-logging-format, licensed as follows:
"""
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

490
README.md
View File

@@ -67,7 +67,9 @@ Ruff is extremely actively developed and used in major open-source projects like
- [Home Assistant](https://github.com/home-assistant/core)
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- [cibuildwheel (PyPA)](https://github.com/pypa/cibuildwheel)
- [build (PyPA)](https://github.com/pypa/build)
- [Babel](https://github.com/python-babel/babel)
- [featuretools](https://github.com/alteryx/featuretools)
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -113,9 +115,9 @@ developer of [Zulip](https://github.com/zulip/zulip):
1. [pycodestyle (E, W)](#pycodestyle-e-w)
1. [mccabe (C90)](#mccabe-c90)
1. [isort (I)](#isort-i)
1. [pep8-naming (N)](#pep8-naming-n)
1. [pydocstyle (D)](#pydocstyle-d)
1. [pyupgrade (UP)](#pyupgrade-up)
1. [pep8-naming (N)](#pep8-naming-n)
1. [flake8-2020 (YTT)](#flake8-2020-ytt)
1. [flake8-annotations (ANN)](#flake8-annotations-ann)
1. [flake8-bandit (S)](#flake8-bandit-s)
@@ -123,30 +125,31 @@ developer of [Zulip](https://github.com/zulip/zulip):
1. [flake8-boolean-trap (FBT)](#flake8-boolean-trap-fbt)
1. [flake8-bugbear (B)](#flake8-bugbear-b)
1. [flake8-builtins (A)](#flake8-builtins-a)
1. [flake8-commas (COM)](#flake8-commas-com)
1. [flake8-comprehensions (C4)](#flake8-comprehensions-c4)
1. [flake8-datetimez (DTZ)](#flake8-datetimez-dtz)
1. [flake8-debugger (T10)](#flake8-debugger-t10)
1. [flake8-errmsg (EM)](#flake8-errmsg-em)
1. [flake8-executable (EXE)](#flake8-executable-exe)
1. [flake8-implicit-str-concat (ISC)](#flake8-implicit-str-concat-isc)
1. [flake8-import-conventions (ICN)](#flake8-import-conventions-icn)
1. [flake8-logging-format (G)](#flake8-logging-format-g)
1. [flake8-no-pep420 (INP)](#flake8-no-pep420-inp)
1. [flake8-pie (PIE)](#flake8-pie-pie)
1. [flake8-print (T20)](#flake8-print-t20)
1. [flake8-pytest-style (PT)](#flake8-pytest-style-pt)
1. [flake8-quotes (Q)](#flake8-quotes-q)
1. [flake8-return (RET)](#flake8-return-ret)
1. [flake8-simplify (SIM)](#flake8-simplify-sim)
1. [flake8-tidy-imports (TID)](#flake8-tidy-imports-tid)
1. [flake8-type-checking (TCH)](#flake8-type-checking-tch)
1. [flake8-unused-arguments (ARG)](#flake8-unused-arguments-arg)
1. [flake8-datetimez (DTZ)](#flake8-datetimez-dtz)
1. [flake8-use-pathlib (PTH)](#flake8-use-pathlib-pth)
1. [eradicate (ERA)](#eradicate-era)
1. [pandas-vet (PD)](#pandas-vet-pd)
1. [pygrep-hooks (PGH)](#pygrep-hooks-pgh)
1. [Pylint (PL)](#pylint-pl)
1. [flake8-pie (PIE)](#flake8-pie-pie)
1. [flake8-commas (COM)](#flake8-commas-com)
1. [flake8-no-pep420 (INP)](#flake8-no-pep420-inp)
1. [flake8-executable (EXE)](#flake8-executable-exe)
1. [flake8-type-checking (TCH)](#flake8-type-checking-tch)
1. [tryceratops (TRY)](#tryceratops-try)
1. [flake8-use-pathlib (PTH)](#flake8-use-pathlib-pth)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf)<!-- End auto-generated table of contents. -->
1. [Editor Integrations](#editor-integrations)
1. [FAQ](#faq)
@@ -213,7 +216,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.235'
rev: 'v0.0.236'
hooks:
- id: ruff
```
@@ -227,12 +230,14 @@ If left unspecified, the default configuration is equivalent to:
```toml
[tool.ruff]
line-length = 88
# Enable Pyflakes `E` and `F` codes by default.
select = ["E", "F"]
ignore = []
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["A", "B", "C", "D", "E", "F", "..."]
unfixable = []
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
@@ -257,6 +262,9 @@ exclude = [
]
per-file-ignores = {}
# Same as Black.
line-length = 88
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
@@ -268,20 +276,21 @@ target-version = "py310"
max-complexity = 10
```
As an example, the following would configure Ruff to: (1) avoid enforcing line-length violations
(`E501`); (2) never remove unused imports (`F401`); and (3) ignore import-at-top-of-file violations
(`E402`) in `__init__.py` files:
As an example, the following would configure Ruff to: (1) enforce flake8-bugbear rules, in addition
to the defaults; (2) avoid enforcing line-length violations (`E501`); (3) avoid attempting to fix
flake8-bugbear (`B`) violations; and (3) ignore import-at-top-of-file violations (`E402`) in
`__init__.py` files:
```toml
[tool.ruff]
# Enable Pyflakes and pycodestyle rules.
select = ["E", "F"]
# Enable flake8-bugbear (`B`) rules.
select = ["E", "F", "B"]
# Never enforce `E501` (line length violations).
ignore = ["E501"]
# Never try to fix `F401` (unused imports).
unfixable = ["F401"]
# Avoid trying to fix flake8-bugbear (`B`) violations.
unfixable = ["B"]
# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
[tool.ruff.per-file-ignores]
@@ -306,10 +315,18 @@ prefix, followed by three digits (e.g., `F401`). The prefix indicates that "sour
rules is determined by the `select` and `ignore` options, which support both the full code (e.g.,
`F401`) and the prefix (e.g., `F`).
As a special-case, Ruff also supports the `ALL` code, which enables all rules. Note that some of the
`pydocstyle` rules conflict (e.g., `D203` and `D211`) as they represent alternative docstring
formats. Enabling `ALL` without further configuration may result in suboptimal behavior, especially
for the `pydocstyle` plugin.
As a special-case, Ruff also supports the `ALL` code, which enables all rules.
If you're wondering how to configure Ruff, here are some **recommended guidelines**:
- Prefer `select` and `ignore` over `extend-select` and `extend-ignore`, to make your rule set
explicit.
- Use `ALL` with discretion. Enabling `ALL` will implicitly enable new rules whenever you upgrade.
- Start with a small set of rules (`select = ["E", "F"]`) and add a category at-a-time. For example,
you might consider expanding to `select = ["E", "F", "B"]` to enable the popular `flake8-bugbear`
extension.
- By default, Ruff's autofix is aggressive. If you find that it's too aggressive for your liking,
consider turning off autofix for specific rules or categories (see: [FAQ](#ruff-tried-to-fix-something-but-it-broke-my-code-what-should-i-do)).
As an alternative to `pyproject.toml`, Ruff will also respect a `ruff.toml` file, which implements
an equivalent schema (though the `[tool.ruff]` hierarchy can be omitted). For example, the
@@ -352,80 +369,71 @@ Arguments:
[FILES]...
Options:
--config <CONFIG>
Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
-v, --verbose
Enable verbose logging
-q, --quiet
Print lint violations, but nothing else
-s, --silent
Disable all logging (but still exit with status code "1" upon detecting lint violations)
-e, --exit-zero
Exit with status code "0", even upon detecting lint violations
-w, --watch
Run in watch mode by re-running whenever files change
--fix
Attempt to automatically fix lint violations
--fix-only
Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`
--diff
Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
-n, --no-cache
Disable cache reads
--isolated
Ignore all configuration files
--fix Attempt to automatically fix lint violations
--show-source Show violations with source code
--diff Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
-w, --watch Run in watch mode by re-running whenever files change
--fix-only Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`
--format <FORMAT> Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab, pylint]
--config <CONFIG> Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
--add-noqa Enable automatic additions of `noqa` directives to failing lines
--show-files See the files Ruff will be run against with the current settings
--show-settings See the settings Ruff will use to lint a given Python file
-h, --help Print help
-V, --version Print version
Rule selection:
--select <RULE_CODE>
Comma-separated list of rule codes to enable (or ALL, to enable all rules)
--extend-select <RULE_CODE>
Like --select, but adds additional rule codes on top of the selected ones
--ignore <RULE_CODE>
Comma-separated list of rule codes to disable
--extend-select <RULE_CODE>
Like --select, but adds additional rule codes on top of the selected ones
--extend-ignore <RULE_CODE>
Like --ignore, but adds additional rule codes on top of the ignored ones
--exclude <FILE_PATTERN>
List of paths, used to omit files and/or directories from analysis
--extend-exclude <FILE_PATTERN>
Like --exclude, but adds additional files and directories on top of those already excluded
--per-file-ignores <PER_FILE_IGNORES>
List of mappings from file pattern to code to exclude
--fixable <RULE_CODE>
List of rule codes to treat as eligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
--unfixable <RULE_CODE>
List of rule codes to treat as ineligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
--per-file-ignores <PER_FILE_IGNORES>
List of mappings from file pattern to code to exclude
--format <FORMAT>
Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab, pylint]
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
--cache-dir <CACHE_DIR>
Path to the cache directory [env: RUFF_CACHE_DIR=]
--show-source
Show violations with source code
--respect-gitignore
Respect file exclusions via `.gitignore` and other standard ignore files
--force-exclude
Enforce exclusions, even for paths passed to Ruff directly on the command-line
--update-check
Enable or disable automatic update checks
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
File selection:
--exclude <FILE_PATTERN> List of paths, used to omit files and/or directories from analysis
--extend-exclude <FILE_PATTERN> Like --exclude, but adds additional files and directories on top of those already excluded
--respect-gitignore Respect file exclusions via `.gitignore` and other standard ignore files
--force-exclude Enforce exclusions, even for paths passed to Ruff directly on the command-line
Rule configuration:
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
--line-length <LINE_LENGTH>
Set the line-length for length-associated rules and automatic formatting
--add-noqa
Enable automatic additions of `noqa` directives to failing lines
--clean
Clear any caches in the current directory or any subdirectories
--explain <EXPLAIN>
Explain a rule
--show-files
See the files Ruff will be run against with the current settings
--show-settings
See the settings Ruff will use to lint a given Python file
-h, --help
Print help information
-V, --version
Print version information
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
Miscellaneous:
-n, --no-cache
Disable cache reads
--isolated
Ignore all configuration files
--cache-dir <CACHE_DIR>
Path to the cache directory [env: RUFF_CACHE_DIR=]
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
-e, --exit-zero
Exit with status code "0", even upon detecting lint violations
--update-check
Enable or disable automatic update checks
Subcommands:
--explain <EXPLAIN> Explain a rule
--clean Clear any caches in the current directory or any subdirectories
Log levels:
-v, --verbose Enable verbose logging
-q, --quiet Print lint violations, but nothing else
-s, --silent Disable all logging (but still exit with status code "1" upon detecting lint violations)
```
<!-- End auto-generated cli help. -->
@@ -548,7 +556,9 @@ add `noqa` directives to all failing lines, with the appropriate rule codes.
Regardless of the rule's origin, Ruff re-implements every rule in Rust as a first-party feature.
By default, Ruff enables all `E` and `F` rule codes, which correspond to those built-in to Flake8.
By default, Ruff enables Flake8's `E` and `F` rules. Ruff supports all rules from the `F` category,
and a [subset](#error-e) of the `E` category, omitting those stylistic rules made obsolete by the
use of an autoformatter, like [Black](https://github.com/psf/black).
The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` command-line option.
@@ -652,6 +662,28 @@ For more, see [isort](https://pypi.org/project/isort/) on PyPI.
| I001 | unsorted-imports | Import block is un-sorted or un-formatted | 🛠 |
| I002 | missing-required-import | Missing required import: `{name}` | 🛠 |
### pep8-naming (N)
For more, see [pep8-naming](https://pypi.org/project/pep8-naming/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| N801 | invalid-class-name | Class name `{name}` should use CapWords convention | |
| N802 | invalid-function-name | Function name `{name}` should be lowercase | |
| N803 | invalid-argument-name | Argument name `{name}` should be lowercase | |
| N804 | invalid-first-argument-name-for-class-method | First argument of a class method should be named `cls` | |
| N805 | invalid-first-argument-name-for-method | First argument of a method should be named `self` | |
| N806 | non-lowercase-variable-in-function | Variable `{name}` in function should be lowercase | |
| N807 | dunder-function-name | Function name should not start and end with `__` | |
| N811 | constant-imported-as-non-constant | Constant `{name}` imported as non-constant `{asname}` | |
| N812 | lowercase-imported-as-non-lowercase | Lowercase `{name}` imported as non-lowercase `{asname}` | |
| N813 | camelcase-imported-as-lowercase | Camelcase `{name}` imported as lowercase `{asname}` | |
| N814 | camelcase-imported-as-constant | Camelcase `{name}` imported as constant `{asname}` | |
| N815 | mixed-case-variable-in-class-scope | Variable `{name}` in class scope should not be mixedCase | |
| N816 | mixed-case-variable-in-global-scope | Variable `{name}` in global scope should not be mixedCase | |
| N817 | camelcase-imported-as-acronym | Camelcase `{name}` imported as acronym `{asname}` | |
| N818 | error-suffix-on-exception-name | Exception name `{name}` should be named with an Error suffix | |
### pydocstyle (D)
For more, see [pydocstyle](https://pypi.org/project/pydocstyle/) on PyPI.
@@ -745,28 +777,6 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/) on PyPI.
| UP033 | functools-cache | Use `@functools.cache` instead of `@functools.lru_cache(maxsize=None)` | 🛠 |
| UP034 | extraneous-parentheses | Avoid extraneous parentheses | 🛠 |
### pep8-naming (N)
For more, see [pep8-naming](https://pypi.org/project/pep8-naming/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| N801 | invalid-class-name | Class name `{name}` should use CapWords convention | |
| N802 | invalid-function-name | Function name `{name}` should be lowercase | |
| N803 | invalid-argument-name | Argument name `{name}` should be lowercase | |
| N804 | invalid-first-argument-name-for-class-method | First argument of a class method should be named `cls` | |
| N805 | invalid-first-argument-name-for-method | First argument of a method should be named `self` | |
| N806 | non-lowercase-variable-in-function | Variable `{name}` in function should be lowercase | |
| N807 | dunder-function-name | Function name should not start and end with `__` | |
| N811 | constant-imported-as-non-constant | Constant `{name}` imported as non-constant `{asname}` | |
| N812 | lowercase-imported-as-non-lowercase | Lowercase `{name}` imported as non-lowercase `{asname}` | |
| N813 | camelcase-imported-as-lowercase | Camelcase `{name}` imported as lowercase `{asname}` | |
| N814 | camelcase-imported-as-constant | Camelcase `{name}` imported as constant `{asname}` | |
| N815 | mixed-case-variable-in-class-scope | Variable `{name}` in class scope should not be mixedCase | |
| N816 | mixed-case-variable-in-global-scope | Variable `{name}` in global scope should not be mixedCase | |
| N817 | camelcase-imported-as-acronym | Camelcase `{name}` imported as acronym `{asname}` | |
| N818 | error-suffix-on-exception-name | Exception name `{name}` should be named with an Error suffix | |
### flake8-2020 (YTT)
For more, see [flake8-2020](https://pypi.org/project/flake8-2020/) on PyPI.
@@ -888,6 +898,16 @@ For more, see [flake8-builtins](https://pypi.org/project/flake8-builtins/) on Py
| A002 | builtin-argument-shadowing | Argument `{name}` is shadowing a python builtin | |
| A003 | builtin-attribute-shadowing | Class attribute `{name}` is shadowing a python builtin | |
### flake8-commas (COM)
For more, see [flake8-commas](https://pypi.org/project/flake8-commas/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| COM812 | trailing-comma-missing | Trailing comma missing | 🛠 |
| COM818 | trailing-comma-on-bare-tuple-prohibited | Trailing comma on bare tuple prohibited | |
| COM819 | trailing-comma-prohibited | Trailing comma prohibited | 🛠 |
### flake8-comprehensions (C4)
For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) on PyPI.
@@ -911,6 +931,22 @@ For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehens
| C416 | unnecessary-comprehension | Unnecessary `{obj_type}` comprehension (rewrite using `{obj_type}()`) | 🛠 |
| C417 | unnecessary-map | Unnecessary `map` usage (rewrite using a generator expression) | |
### flake8-datetimez (DTZ)
For more, see [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| DTZ001 | call-datetime-without-tzinfo | The use of `datetime.datetime()` without `tzinfo` argument is not allowed | |
| DTZ002 | call-datetime-today | The use of `datetime.datetime.today()` is not allowed | |
| DTZ003 | call-datetime-utcnow | The use of `datetime.datetime.utcnow()` is not allowed | |
| DTZ004 | call-datetime-utcfromtimestamp | The use of `datetime.datetime.utcfromtimestamp()` is not allowed | |
| DTZ005 | call-datetime-now-without-tzinfo | The use of `datetime.datetime.now()` without `tz` argument is not allowed | |
| DTZ006 | call-datetime-fromtimestamp | The use of `datetime.datetime.fromtimestamp()` without `tz` argument is not allowed | |
| DTZ007 | call-datetime-strptime-without-zone | The use of `datetime.datetime.strptime()` without %z must be followed by `.replace(tzinfo=)` or `.astimezone()` | |
| DTZ011 | call-date-today | The use of `datetime.date.today()` is not allowed. | |
| DTZ012 | call-date-fromtimestamp | The use of `datetime.date.fromtimestamp()` is not allowed | |
### flake8-debugger (T10)
For more, see [flake8-debugger](https://pypi.org/project/flake8-debugger/) on PyPI.
@@ -929,6 +965,18 @@ For more, see [flake8-errmsg](https://pypi.org/project/flake8-errmsg/) on PyPI.
| EM102 | f-string-in-exception | Exception must not use an f-string literal, assign to variable first | |
| EM103 | dot-format-in-exception | Exception must not use a `.format()` string directly, assign to variable first | |
### flake8-executable (EXE)
For more, see [flake8-executable](https://pypi.org/project/flake8-executable/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| EXE001 | shebang-not-executable | Shebang is present but file is not executable | |
| EXE002 | shebang-missing-executable-file | The file is executable but no shebang is present | |
| EXE003 | shebang-python | Shebang should contain "python" | |
| EXE004 | shebang-whitespace | Avoid whitespace before shebang | 🛠 |
| EXE005 | shebang-newline | Shebang should be at the beginning of the file | |
### flake8-implicit-str-concat (ISC)
For more, see [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/) on PyPI.
@@ -947,6 +995,42 @@ For more, see [flake8-import-conventions](https://github.com/joaopalmeiro/flake8
| ---- | ---- | ------- | --- |
| ICN001 | import-alias-is-not-conventional | `{name}` should be imported as `{asname}` | |
### flake8-logging-format (G)
For more, see [flake8-logging-format](https://pypi.org/project/flake8-logging-format/0.9.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| G001 | logging-string-format | Logging statement uses `string.format()` | |
| G002 | logging-percent-format | Logging statement uses `%` | |
| G003 | logging-string-concat | Logging statement uses `+` | |
| G004 | logging-f-string | Logging statement uses f-string | |
| G010 | logging-warn | Logging statement uses `warn` instead of `warning` | 🛠 |
| G101 | logging-extra-attr-clash | Logging statement uses an extra field that clashes with a LogRecord field: `{key}` | |
| G201 | logging-exc-info | Logging `.exception(...)` should be used instead of `.error(..., exc_info=True)` | |
| G202 | logging-redundant-exc-info | Logging statement has redundant `exc_info` | |
### flake8-no-pep420 (INP)
For more, see [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| INP001 | implicit-namespace-package | File `{filename}` is part of an implicit namespace package. Add an `__init__.py`. | |
### flake8-pie (PIE)
For more, see [flake8-pie](https://pypi.org/project/flake8-pie/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PIE790 | no-unnecessary-pass | Unnecessary `pass` statement | 🛠 |
| PIE794 | dupe-class-field-definitions | Class field `{name}` is defined multiple times | 🛠 |
| PIE796 | prefer-unique-enums | Enum contains duplicate value: `{value}` | |
| PIE800 | no-unnecessary-spread | Unnecessary spread `**` | |
| PIE804 | no-unnecessary-dict-kwargs | Unnecessary `dict` kwargs | |
| PIE807 | prefer-list-builtin | Prefer `list` over useless lambda | 🛠 |
### flake8-print (T20)
For more, see [flake8-print](https://pypi.org/project/flake8-print/) on PyPI.
@@ -1055,6 +1139,18 @@ For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports
| TID251 | banned-api | `{name}` is banned: {message} | |
| TID252 | relative-imports | Relative imports from parent modules are banned | |
### flake8-type-checking (TCH)
For more, see [flake8-type-checking](https://pypi.org/project/flake8-type-checking/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TCH001 | typing-only-first-party-import | Move application import `{}` into a type-checking block | |
| TCH002 | typing-only-third-party-import | Move third-party import `{}` into a type-checking block | |
| TCH003 | typing-only-standard-library-import | Move standard library import `{}` into a type-checking block | |
| TCH004 | runtime-import-in-type-checking-block | Move import `{}` out of type-checking block. Import is used for more than type hinting. | |
| TCH005 | empty-type-checking-block | Found empty type-checking block | |
### flake8-unused-arguments (ARG)
For more, see [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/) on PyPI.
@@ -1067,21 +1163,37 @@ For more, see [flake8-unused-arguments](https://pypi.org/project/flake8-unused-a
| ARG004 | unused-static-method-argument | Unused static method argument: `{name}` | |
| ARG005 | unused-lambda-argument | Unused lambda argument: `{name}` | |
### flake8-datetimez (DTZ)
### flake8-use-pathlib (PTH)
For more, see [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) on PyPI.
For more, see [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| DTZ001 | call-datetime-without-tzinfo | The use of `datetime.datetime()` without `tzinfo` argument is not allowed | |
| DTZ002 | call-datetime-today | The use of `datetime.datetime.today()` is not allowed | |
| DTZ003 | call-datetime-utcnow | The use of `datetime.datetime.utcnow()` is not allowed | |
| DTZ004 | call-datetime-utcfromtimestamp | The use of `datetime.datetime.utcfromtimestamp()` is not allowed | |
| DTZ005 | call-datetime-now-without-tzinfo | The use of `datetime.datetime.now()` without `tz` argument is not allowed | |
| DTZ006 | call-datetime-fromtimestamp | The use of `datetime.datetime.fromtimestamp()` without `tz` argument is not allowed | |
| DTZ007 | call-datetime-strptime-without-zone | The use of `datetime.datetime.strptime()` without %z must be followed by `.replace(tzinfo=)` or `.astimezone()` | |
| DTZ011 | call-date-today | The use of `datetime.date.today()` is not allowed. | |
| DTZ012 | call-date-fromtimestamp | The use of `datetime.date.fromtimestamp()` is not allowed | |
| PTH100 | pathlib-abspath | `os.path.abspath` should be replaced by `.resolve()` | |
| PTH101 | pathlib-chmod | `os.chmod` should be replaced by `.chmod()` | |
| PTH102 | pathlib-mkdir | `os.mkdir` should be replaced by `.mkdir()` | |
| PTH103 | pathlib-makedirs | `os.makedirs` should be replaced by `.mkdir(parents=True)` | |
| PTH104 | pathlib-rename | `os.rename` should be replaced by `.rename()` | |
| PTH105 | pathlib-replace | `os.replace`should be replaced by `.replace()` | |
| PTH106 | pathlib-rmdir | `os.rmdir` should be replaced by `.rmdir()` | |
| PTH107 | pathlib-remove | `os.remove` should be replaced by `.unlink()` | |
| PTH108 | pathlib-unlink | `os.unlink` should be replaced by `.unlink()` | |
| PTH109 | pathlib-getcwd | `os.getcwd()` should be replaced by `Path.cwd()` | |
| PTH110 | pathlib-exists | `os.path.exists` should be replaced by `.exists()` | |
| PTH111 | pathlib-expanduser | `os.path.expanduser` should be replaced by `.expanduser()` | |
| PTH112 | pathlib-is-dir | `os.path.isdir` should be replaced by `.is_dir()` | |
| PTH113 | pathlib-is-file | `os.path.isfile` should be replaced by `.is_file()` | |
| PTH114 | pathlib-is-link | `os.path.islink` should be replaced by `.is_symlink()` | |
| PTH115 | pathlib-readlink | `os.readlink(` should be replaced by `.readlink()` | |
| PTH116 | pathlib-stat | `os.stat` should be replaced by `.stat()` or `.owner()` or `.group()` | |
| PTH117 | pathlib-is-abs | `os.path.isabs` should be replaced by `.is_absolute()` | |
| PTH118 | pathlib-join | `os.path.join` should be replaced by foo_path / "bar" | |
| PTH119 | pathlib-basename | `os.path.basename` should be replaced by `.name` | |
| PTH120 | pathlib-dirname | `os.path.dirname` should be replaced by `.parent` | |
| PTH121 | pathlib-samefile | `os.path.samefile` should be replaced by `.samefile()` | |
| PTH122 | pathlib-splitext | `os.path.splitext` should be replaced by `.suffix` | |
| PTH123 | pathlib-open | `open("foo")` should be replaced by`Path("foo").open()` | |
| PTH124 | pathlib-py-path | `py.path` is in maintenance mode, use `pathlib` instead | |
### eradicate (ERA)
@@ -1154,104 +1266,20 @@ For more, see [Pylint](https://pypi.org/project/pylint/) on PyPI.
| PLW0120 | useless-else-on-loop | Else clause on loop without a break statement, remove the else and de-indent all the code inside it | |
| PLW0602 | global-variable-not-assigned | Using global for `{name}` but no assignment is done | |
### flake8-pie (PIE)
For more, see [flake8-pie](https://pypi.org/project/flake8-pie/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PIE790 | no-unnecessary-pass | Unnecessary `pass` statement | 🛠 |
| PIE794 | dupe-class-field-definitions | Class field `{name}` is defined multiple times | 🛠 |
| PIE796 | prefer-unique-enums | Enum contains duplicate value: `{value}` | |
| PIE800 | no-unnecessary-spread | Unnecessary spread `**` | |
| PIE804 | no-unnecessary-dict-kwargs | Unnecessary `dict` kwargs | |
| PIE807 | prefer-list-builtin | Prefer `list` over useless lambda | 🛠 |
### flake8-commas (COM)
For more, see [flake8-commas](https://pypi.org/project/flake8-commas/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| COM812 | trailing-comma-missing | Trailing comma missing | 🛠 |
| COM818 | trailing-comma-on-bare-tuple-prohibited | Trailing comma on bare tuple prohibited | |
| COM819 | trailing-comma-prohibited | Trailing comma prohibited | 🛠 |
### flake8-no-pep420 (INP)
For more, see [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| INP001 | implicit-namespace-package | File `{filename}` is part of an implicit namespace package. Add an `__init__.py`. | |
### flake8-executable (EXE)
For more, see [flake8-executable](https://pypi.org/project/flake8-executable/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| EXE001 | shebang-not-executable | Shebang is present but file is not executable | |
| EXE002 | shebang-missing-executable-file | The file is executable but no shebang is present | |
| EXE003 | shebang-python | Shebang should contain "python" | |
| EXE004 | shebang-whitespace | Avoid whitespace before shebang | 🛠 |
| EXE005 | shebang-newline | Shebang should be at the beginning of the file | |
### flake8-type-checking (TCH)
For more, see [flake8-type-checking](https://pypi.org/project/flake8-type-checking/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TCH001 | typing-only-first-party-import | Move application import `{}` into a type-checking block | |
| TCH002 | typing-only-third-party-import | Move third-party import `{}` into a type-checking block | |
| TCH003 | typing-only-standard-library-import | Move standard library import `{}` into a type-checking block | |
| TCH004 | runtime-import-in-type-checking-block | Move import `{}` out of type-checking block. Import is used for more than type hinting. | |
| TCH005 | empty-type-checking-block | Found empty type-checking block | |
### tryceratops (TRY)
For more, see [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TRY002 | raise-vanilla-class | Create your own exception | |
| TRY003 | raise-vanilla-args | Avoid specifying long messages outside the exception class | |
| TRY004 | prefer-type-error | Prefer `TypeError` exception for invalid type | 🛠 |
| TRY200 | reraise-no-cause | Use `raise from` to specify exception cause | |
| TRY201 | verbose-raise | Use `raise` without specifying exception name | |
| TRY300 | try-consider-else | Consider `else` block | |
| TRY300 | try-consider-else | Consider moving this statement to an `else` block | |
| TRY301 | raise-within-try | Abstract `raise` to an inner function | |
### flake8-use-pathlib (PTH)
For more, see [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PTH100 | pathlib-abspath | `os.path.abspath` should be replaced by `.resolve()` | |
| PTH101 | pathlib-chmod | `os.chmod` should be replaced by `.chmod()` | |
| PTH102 | pathlib-mkdir | `os.mkdir` should be replaced by `.mkdir()` | |
| PTH103 | pathlib-makedirs | `os.makedirs` should be replaced by `.mkdir(parents=True)` | |
| PTH104 | pathlib-rename | `os.rename` should be replaced by `.rename()` | |
| PTH105 | pathlib-replace | `os.replace`should be replaced by `.replace()` | |
| PTH106 | pathlib-rmdir | `os.rmdir` should be replaced by `.rmdir()` | |
| PTH107 | pathlib-remove | `os.remove` should be replaced by `.unlink()` | |
| PTH108 | pathlib-unlink | `os.unlink` should be replaced by `.unlink()` | |
| PTH109 | pathlib-getcwd | `os.getcwd()` should be replaced by `Path.cwd()` | |
| PTH110 | pathlib-exists | `os.path.exists` should be replaced by `.exists()` | |
| PTH111 | pathlib-expanduser | `os.path.expanduser` should be replaced by `.expanduser()` | |
| PTH112 | pathlib-is-dir | `os.path.isdir` should be replaced by `.is_dir()` | |
| PTH113 | pathlib-is-file | `os.path.isfile` should be replaced by `.is_file()` | |
| PTH114 | pathlib-is-link | `os.path.islink` should be replaced by `.is_symlink()` | |
| PTH115 | pathlib-readlink | `os.readlink(` should be replaced by `.readlink()` | |
| PTH116 | pathlib-stat | `os.stat` should be replaced by `.stat()` or `.owner()` or `.group()` | |
| PTH117 | pathlib-is-abs | `os.path.isabs` should be replaced by `.is_absolute()` | |
| PTH118 | pathlib-join | `os.path.join` should be replaced by foo_path / "bar" | |
| PTH119 | pathlib-basename | `os.path.basename` should be replaced by `.name` | |
| PTH120 | pathlib-dirname | `os.path.dirname` should be replaced by `.parent` | |
| PTH121 | pathlib-samefile | `os.path.samefile` should be replaced by `.samefile()` | |
| PTH122 | pathlib-splitext | `os.path.splitext` should be replaced by `.suffix` | |
| PTH123 | pathlib-open | `open("foo")` should be replaced by`Path("foo").open()` | |
| PTH124 | pathlib-py-path | `py.path` is in maintenance mode, use `pathlib` instead | |
| TRY400 | error-instead-of-exception | Use `logging.exception` instead of `logging.error` | |
### Ruff-specific rules (RUF)
@@ -1524,7 +1552,9 @@ automatically convert your existing configuration.)
Ruff can be used as a 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 implements every rule in Flake8.
Under those conditions, Ruff implements every rule in Flake8. In practice, that means Ruff
implements all of the `F` rules (which originate from Pyflakes), along with a subset of the `E` and
`W` rules (which originate from pycodestyle).
Ruff also re-implements some of the most popular Flake8 plugins and related code quality tools
natively, including:
@@ -1548,6 +1578,7 @@ natively, including:
- [`flake8-executable`](https://pypi.org/project/flake8-executable/)
- [`flake8-implicit-str-concat`](https://pypi.org/project/flake8-implicit-str-concat/)
- [`flake8-import-conventions`](https://github.com/joaopalmeiro/flake8-import-conventions)
- [`flake8-logging-format](https://pypi.org/project/flake8-logging-format/)
- [`flake8-no-pep420`](https://pypi.org/project/flake8-no-pep420)
- [`flake8-pie`](https://pypi.org/project/flake8-pie/) ([#1543](https://github.com/charliermarsh/ruff/issues/1543))
- [`flake8-print`](https://pypi.org/project/flake8-print/)
@@ -1557,6 +1588,8 @@ natively, including:
- [`flake8-simplify`](https://pypi.org/project/flake8-simplify/) ([#998](https://github.com/charliermarsh/ruff/issues/998))
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/)
- [`flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
- [`flake8-use-pathlib`](https://pypi.org/project/flake8-use-pathlib/)
- [`isort`](https://pypi.org/project/isort/)
- [`mccabe`](https://pypi.org/project/mccabe/)
- [`pandas-vet`](https://pypi.org/project/pandas-vet/)
@@ -1618,6 +1651,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-executable`](https://pypi.org/project/flake8-executable/)
- [`flake8-implicit-str-concat`](https://pypi.org/project/flake8-implicit-str-concat/)
- [`flake8-import-conventions`](https://github.com/joaopalmeiro/flake8-import-conventions)
- [`flake8-logging-format](https://pypi.org/project/flake8-logging-format/)
- [`flake8-no-pep420`](https://pypi.org/project/flake8-no-pep420)
- [`flake8-pie`](https://pypi.org/project/flake8-pie/) ([#1543](https://github.com/charliermarsh/ruff/issues/1543))
- [`flake8-print`](https://pypi.org/project/flake8-print/)
@@ -1627,6 +1661,8 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-simplify`](https://pypi.org/project/flake8-simplify/) ([#998](https://github.com/charliermarsh/ruff/issues/998))
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/)
- [`flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
- [`flake8-use-pathlib`](https://pypi.org/project/flake8-use-pathlib/)
- [`mccabe`](https://pypi.org/project/mccabe/)
- [`pandas-vet`](https://pypi.org/project/pandas-vet/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
@@ -1751,7 +1787,6 @@ unfixable = ["B", "SIM", "TRY", "RUF"]
If you find a case where Ruff's autofix breaks your code, please file an Issue!
## Contributing
Contributions are welcome and hugely appreciated. To get started, check out the
@@ -2374,7 +2409,7 @@ Enabled by default.
```toml
[tool.ruff]
respect_gitignore = false
respect-gitignore = false
```
---
@@ -2439,7 +2474,7 @@ my_package/
bar.py
```
The `src` directory should be included in `source` (e.g., `source =
The `src` directory should be included in the `src` option (e.g., `src =
["src"]`), such that when resolving imports, `my_package.foo` is
considered a first-party import.
@@ -3076,6 +3111,45 @@ and can be circumvented via `eval` or `importlib`.
---
### `flake8-type-checking`
#### [`exempt-modules`](#exempt-modules)
Exempt certain modules from needing to be moved into type-checking
blocks.
**Default value**: `[]`
**Type**: `Vec<String>`
**Example usage**:
```toml
[tool.ruff.flake8-type-checking]
exempt-modules = ["typing_extensions"]
```
---
#### [`strict`](#strict)
Enforce TC001, TC002, and TC003 rules even when valid runtime imports
are present for the same module.
See: https://github.com/snok/flake8-type-checking#strict.
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff.flake8-type-checking]
strict = true
```
---
### `flake8-unused-arguments`
#### [`ignore-variadic-names`](#ignore-variadic-names)

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.235"
version = "0.0.236"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.235"
version = "0.0.236"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.235"
version = "0.0.236"
edition = "2021"
[dependencies]

View File

@@ -26,7 +26,7 @@ use ruff::flake8_to_ruff::{self, ExternalConfig};
about = "Convert existing Flake8 configuration to Ruff.",
long_about = None
)]
struct Cli {
struct Args {
/// Path to the Flake8 configuration file (e.g., `setup.cfg`, `tox.ini`, or
/// `.flake8`).
#[arg(required = true)]
@@ -41,15 +41,15 @@ struct Cli {
}
fn main() -> Result<()> {
let cli = Cli::parse();
let args = Args::parse();
// Read the INI file.
let mut ini = Ini::new_cs();
ini.set_multiline(true);
let config = ini.load(cli.file).map_err(|msg| anyhow::anyhow!(msg))?;
let config = ini.load(args.file).map_err(|msg| anyhow::anyhow!(msg))?;
// Read the pyproject.toml file.
let pyproject = cli.pyproject.map(flake8_to_ruff::parse).transpose()?;
let pyproject = args.pyproject.map(flake8_to_ruff::parse).transpose()?;
let external_config = pyproject
.as_ref()
.and_then(|pyproject| pyproject.tool.as_ref())
@@ -60,7 +60,7 @@ fn main() -> Result<()> {
.unwrap_or_default();
// Create Ruff's pyproject.toml section.
let pyproject = flake8_to_ruff::convert(&config, &external_config, cli.plugin)?;
let pyproject = flake8_to_ruff::convert(&config, &external_config, args.plugin)?;
println!("{}", toml::to_string_pretty(&pyproject)?);
Ok(())

View File

@@ -7,7 +7,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.235"
version = "0.0.236"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },

View File

@@ -61,3 +61,34 @@ except Exception as e:
raise bad
except BaseException:
pass
import logging
try:
pass
except Exception:
logging.error("...")
try:
pass
except Exception:
logging.error("...", exc_info=False)
try:
pass
except Exception:
logging.error("...", exc_info=None)
try:
pass
except Exception:
logging.exception("...")
try:
pass
except Exception:
logging.error("...", exc_info=True)

View File

@@ -1,7 +1,7 @@
"""
Should emit:
B009 - Line 18, 19, 20, 21, 22
B010 - Line 33, 34, 35, 36
B009 - Line 19, 20, 21, 22, 23, 24
B010 - Line 40, 41, 42, 43, 44, 45
"""
# Valid getattr usage
@@ -13,10 +13,12 @@ getattr(foo, bar, None)
getattr(foo, "123abc")
getattr(foo, r"123\abc")
getattr(foo, "except")
getattr(foo, "__123abc")
# Invalid usage
getattr(foo, "bar")
getattr(foo, "_123abc")
getattr(foo, "__123abc__")
getattr(foo, "abc123")
getattr(foo, r"abc123")
_ = lambda x: getattr(x, "bar")
@@ -27,6 +29,7 @@ if getattr(x, "bar"):
setattr(foo, bar, None)
setattr(foo, "bar{foo}".format(foo="a"), None)
setattr(foo, "123abc", None)
setattr(foo, "__123abc", None)
setattr(foo, r"123\abc", None)
setattr(foo, "except", None)
_ = lambda x: setattr(x, "bar", 1)
@@ -36,6 +39,7 @@ if setattr(x, "bar", 1):
# Invalid usage
setattr(foo, "bar", None)
setattr(foo, "_123abc", None)
setattr(foo, "__123abc__", None)
setattr(foo, "abc123", None)
setattr(foo, r"abc123", None)
setattr(foo.bar, r"baz", None)

View File

@@ -0,0 +1,3 @@
import logging
logging.info("Hello {}".format("World!"))

View File

@@ -0,0 +1,3 @@
import logging
logging.info("Hello %s" % "World!")

View File

@@ -0,0 +1,3 @@
import logging
logging.info("Hello" + " " + "World!")

View File

@@ -0,0 +1,4 @@
import logging
name = "world"
logging.info(f"Hello {name}")

View File

@@ -0,0 +1,3 @@
import logging
logging.warn("Hello World!")

View File

@@ -0,0 +1,8 @@
import logging
logging.info(
"Hello world!",
extra={
"name": "foobar",
},
)

View File

@@ -0,0 +1,8 @@
import logging
logging.info(
"Hello world!",
extra=dict(
name="foobar",
),
)

View File

@@ -0,0 +1,3 @@
import logging
logging.error('Hello World', exc_info=True)

View File

@@ -0,0 +1,3 @@
import logging
logging.exception('Hello World', exc_info=True)

View File

@@ -0,0 +1,7 @@
import argparse
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument("target_dir", type=Path)
args = parser.parse_args()
parser.error(f"Target directory {args.target_dir} does not exist")

View File

@@ -0,0 +1,8 @@
import logging
logging.info(
"Hello {world}!",
extra=dict(
world="World",
),
)

View File

@@ -0,0 +1,8 @@
import logging
logging.info(
"Hello {world}!",
extra=dict(
world="{}".format("World"),
),
)

View File

@@ -0,0 +1,3 @@
import logging
logging.info("Hello World!")

View File

@@ -0,0 +1,3 @@
import warnings
warnings.warn("Hello World!")

View File

@@ -1,12 +1,12 @@
class Foo:
"""buzz"""
pass # PIE790
pass
if foo:
"""foo"""
pass # PIE790
pass
def multi_statement() -> None:
@@ -18,28 +18,28 @@ if foo:
pass
else:
"""bar"""
pass # PIE790
pass
while True:
pass
else:
"""bar"""
pass # PIE790
pass
for _ in range(10):
pass
else:
"""bar"""
pass # PIE790
pass
async for _ in range(10):
pass
else:
"""bar"""
pass # PIE790
pass
def foo() -> None:
@@ -47,7 +47,7 @@ def foo() -> None:
buzz
"""
pass # PIE790
pass
async def foo():
@@ -55,14 +55,14 @@ async def foo():
buzz
"""
pass # PIE790
pass
try:
"""
buzz
"""
pass # PIE790
pass
except ValueError:
pass
@@ -71,29 +71,34 @@ try:
bar()
except ValueError:
"""bar"""
pass # PIE790
pass
for _ in range(10):
"""buzz"""
pass # PIE790
pass
async for _ in range(10):
"""buzz"""
pass # PIE790
pass
while cond:
"""buzz"""
pass # PIE790
pass
with bar:
"""buzz"""
pass # PIE790
pass
async with bar:
"""buzz"""
pass # PIE790
pass
def foo() -> None:
"""buzz"""
pass # bar
class Foo:

View File

@@ -0,0 +1,16 @@
def f():
import pandas as pd
x: pd.DataFrame
def f():
import pandas.core.frame as pd
x: pd.DataFrame
def f():
import flask
x: flask

View File

@@ -0,0 +1,54 @@
def f():
# Even in strict mode, this shouldn't rase an error, since `pkg` is used at runtime,
# and implicitly imports `pkg.bar`.
import pkg
import pkg.bar
def test(value: pkg.bar.A):
return pkg.B()
def f():
# Even in strict mode, this shouldn't rase an error, since `pkg.bar` is used at
# runtime, and implicitly imports `pkg`.
import pkg
import pkg.bar
def test(value: pkg.A):
return pkg.bar.B()
def f():
# In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime.
import pkg
from pkg import A
def test(value: A):
return pkg.B()
def f():
# In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime.
from pkg import A, B
def test(value: A):
return B()
def f():
# Even in strict mode, this shouldn't rase an error, since `pkg.baz` is used at
# runtime, and implicitly imports `pkg.bar`.
import pkg.bar
import pkg.baz
def test(value: pkg.bar.A):
return pkg.baz.B()
def f():
# In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime.
import pkg
from pkg.bar import A
def test(value: A):
return pkg.B()

View File

@@ -0,0 +1,31 @@
"""
Violation:
Raising vanilla exception with custom message means it should be customized.
"""
from somewhere import exceptions
def func():
a = 1
if a == 1:
raise Exception("Custom message")
b = 1
if b == 1:
raise Exception
def ignore():
try:
a = 1
except Exception as ex:
# This is another violation, but this specific analyzer shouldn't care
raise ex
def anotherfunc():
a = 1
if a == 1:
raise exceptions.Exception("Another except") # That's fine

View File

@@ -0,0 +1,54 @@
class CustomException(Exception):
pass
def func():
a = 1
if a == 1:
raise CustomException("Long message")
elif a == 2:
raise CustomException("Short") # This is acceptable
elif a == 3:
raise CustomException("its_code_not_message") # This is acceptable
def ignore():
try:
a = 1
except Exception as ex:
# This is another violation, but this specific analyzer shouldn't care
raise ex
class BadArgCantBeEven(Exception):
pass
class GoodArgCantBeEven(Exception):
def __init__(self, arg):
super().__init__(f"The argument '{arg}' should be even")
def bad(a):
if a % 2 == 0:
raise BadArgCantBeEven(f"The argument '{a}' should be even")
def another_bad(a):
if a % 2 == 0:
raise BadArgCantBeEven(f"The argument {a} should not be odd.")
def and_another_bad(a):
if a % 2 == 0:
raise BadArgCantBeEven("The argument `a` should not be odd.")
def good(a: int):
if a % 2 == 0:
raise GoodArgCantBeEven(a)
def another_good(a):
if a % 2 == 0:
raise GoodArgCantBeEven(a)

View File

@@ -0,0 +1,52 @@
"""
Violation:
Use '.exception' over '.error' inside except blocks
"""
import logging
logger = logging.getLogger(__name__)
def bad():
try:
a = 1
except Exception:
logging.error("Context message here")
if True:
logging.error("Context message here")
def bad():
try:
a = 1
except Exception:
logger.error("Context message here")
if True:
logger.error("Context message here")
def bad():
try:
a = 1
except Exception:
self.logger.error("Context message here")
if True:
self.logger.error("Context message here")
def good():
try:
a = 1
except Exception:
logger.exception("Context message here")
def good():
try:
a = 1
except Exception:
foo.exception("Context message here")

View File

@@ -230,6 +230,17 @@
}
]
},
"flake8-type-checking": {
"description": "Options for the `flake8-type-checking` plugin.",
"anyOf": [
{
"$ref": "#/definitions/Flake8TypeCheckingOptions"
},
{
"type": "null"
}
]
},
"flake8-unused-arguments": {
"description": "Options for the `flake8-unused-arguments` plugin.",
"anyOf": [
@@ -421,7 +432,7 @@
]
},
"src": {
"description": "The source code paths to consider, e.g., when resolving first- vs. third-party imports.\n\nAs an example: given a Python package structure like:\n\n```text my_package/ pyproject.toml src/ my_package/ __init__.py foo.py bar.py ```\n\nThe `src` directory should be included in `source` (e.g., `source = [\"src\"]`), such that when resolving imports, `my_package.foo` is considered a first-party import.\n\nThis field supports globs. For example, if you have a series of Python packages in a `python_modules` directory, `src = [\"python_modules/*\"]` would expand to incorporate all of the packages in that directory. User home directory and environment variables will also be expanded.",
"description": "The source code paths to consider, e.g., when resolving first- vs. third-party imports.\n\nAs an example: given a Python package structure like:\n\n```text my_package/ pyproject.toml src/ my_package/ __init__.py foo.py bar.py ```\n\nThe `src` directory should be included in the `src` option (e.g., `src = [\"src\"]`), such that when resolving imports, `my_package.foo` is considered a first-party import.\n\nThis field supports globs. For example, if you have a series of Python packages in a `python_modules` directory, `src = [\"python_modules/*\"]` would expand to incorporate all of the packages in that directory. User home directory and environment variables will also be expanded.",
"type": [
"array",
"null"
@@ -822,6 +833,29 @@
},
"additionalProperties": false
},
"Flake8TypeCheckingOptions": {
"type": "object",
"properties": {
"exempt-modules": {
"description": "Exempt certain modules from needing to be moved into type-checking blocks.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"strict": {
"description": "Enforce TC001, TC002, and TC003 rules even when valid runtime imports are present for the same module. See: https://github.com/snok/flake8-type-checking#strict.",
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false
},
"Flake8UnusedArgumentsOptions": {
"type": "object",
"properties": {
@@ -1475,6 +1509,22 @@
"FBT001",
"FBT002",
"FBT003",
"G",
"G0",
"G00",
"G001",
"G002",
"G003",
"G004",
"G01",
"G010",
"G1",
"G10",
"G101",
"G2",
"G20",
"G201",
"G202",
"I",
"I0",
"I00",
@@ -1776,6 +1826,8 @@
"TRY",
"TRY0",
"TRY00",
"TRY002",
"TRY003",
"TRY004",
"TRY2",
"TRY20",
@@ -1785,6 +1837,9 @@
"TRY30",
"TRY300",
"TRY301",
"TRY4",
"TRY40",
"TRY400",
"UP",
"UP0",
"UP00",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.235"
version = "0.0.236"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"

View File

@@ -19,124 +19,166 @@ use rustc_hash::FxHashMap;
)]
#[command(version)]
#[allow(clippy::struct_excessive_bools)]
pub struct Cli {
pub struct Args {
#[arg(required_unless_present_any = ["clean", "explain", "generate_shell_completion"])]
pub files: Vec<PathBuf>,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for
/// configuration.
#[arg(long, conflicts_with = "isolated")]
pub config: Option<PathBuf>,
/// Enable verbose logging.
#[arg(short, long, group = "verbosity")]
pub verbose: bool,
/// Print lint violations, but nothing else.
#[arg(short, long, group = "verbosity")]
pub quiet: bool,
/// Disable all logging (but still exit with status code "1" upon detecting
/// lint violations).
#[arg(short, long, group = "verbosity")]
pub silent: bool,
/// Exit with status code "0", even upon detecting lint violations.
#[arg(short, long)]
pub exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
pub watch: bool,
/// Attempt to automatically fix lint violations.
#[arg(long, overrides_with("no_fix"))]
fix: bool,
#[clap(long, overrides_with("fix"), hide = true)]
no_fix: bool,
/// Show violations with source code.
#[arg(long, overrides_with("no_show_source"))]
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// Avoid writing any fixed files back; instead, output a diff for each
/// changed file to stdout.
#[arg(long)]
pub diff: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
pub watch: bool,
/// Fix any fixable lint violations, but don't report on leftover
/// violations. Implies `--fix`.
#[arg(long, overrides_with("no_fix_only"))]
fix_only: bool,
#[clap(long, overrides_with("fix_only"), hide = true)]
no_fix_only: bool,
/// Avoid writing any fixed files back; instead, output a diff for each
/// changed file to stdout.
#[arg(long)]
pub diff: bool,
/// Disable cache reads.
#[arg(short, long)]
pub no_cache: bool,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config")]
pub isolated: bool,
/// Comma-separated list of rule codes to enable (or ALL, to enable all
/// rules).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub select: Option<Vec<RuleSelector>>,
/// Like --select, but adds additional rule codes on top of the selected
/// ones.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub extend_select: Option<Vec<RuleSelector>>,
/// Comma-separated list of rule codes to disable.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub ignore: Option<Vec<RuleSelector>>,
/// Like --ignore, but adds additional rule codes on top of the ignored
/// ones.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub extend_ignore: Option<Vec<RuleSelector>>,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(long, value_delimiter = ',', value_name = "FILE_PATTERN")]
pub exclude: Option<Vec<FilePattern>>,
/// Like --exclude, but adds additional files and directories on top of
/// those already excluded.
#[arg(long, value_delimiter = ',', value_name = "FILE_PATTERN")]
pub extend_exclude: Option<Vec<FilePattern>>,
/// List of rule codes to treat as eligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub fixable: Option<Vec<RuleSelector>>,
/// List of rule codes to treat as ineligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub unfixable: Option<Vec<RuleSelector>>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// Output serialization format for violations.
#[arg(long, value_enum, env = "RUFF_FORMAT")]
pub format: Option<SerializationFormat>,
/// The name of the file when passing it through stdin.
#[arg(long)]
pub stdin_filename: Option<PathBuf>,
/// Path to the cache directory.
#[arg(long, env = "RUFF_CACHE_DIR")]
pub cache_dir: Option<PathBuf>,
/// Show violations with source code.
#[arg(long, overrides_with("no_show_source"))]
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for
/// configuration.
#[arg(long, conflicts_with = "isolated")]
pub config: Option<PathBuf>,
/// Comma-separated list of rule codes to enable (or ALL, to enable all
/// rules).
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub select: Option<Vec<RuleSelector>>,
/// Comma-separated list of rule codes to disable.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub ignore: Option<Vec<RuleSelector>>,
/// Like --select, but adds additional rule codes on top of the selected
/// ones.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub extend_select: Option<Vec<RuleSelector>>,
/// Like --ignore, but adds additional rule codes on top of the ignored
/// ones.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub extend_ignore: Option<Vec<RuleSelector>>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',', help_heading = "Rule selection")]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN",
help_heading = "File selection"
)]
pub exclude: Option<Vec<FilePattern>>,
/// Like --exclude, but adds additional files and directories on top of
/// those already excluded.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN",
help_heading = "File selection"
)]
pub extend_exclude: Option<Vec<FilePattern>>,
/// List of rule codes to treat as eligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub fixable: Option<Vec<RuleSelector>>,
/// List of rule codes to treat as ineligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub unfixable: Option<Vec<RuleSelector>>,
/// Respect file exclusions via `.gitignore` and other standard ignore
/// files.
#[arg(long, overrides_with("no_respect_gitignore"))]
#[arg(
long,
overrides_with("no_respect_gitignore"),
help_heading = "File selection"
)]
respect_gitignore: bool,
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
no_respect_gitignore: bool,
/// Enforce exclusions, even for paths passed to Ruff directly on the
/// command-line.
#[arg(long, overrides_with("no_force_exclude"))]
#[arg(
long,
overrides_with("no_force_exclude"),
help_heading = "File selection"
)]
force_exclude: bool,
#[clap(long, overrides_with("force_exclude"), hide = true)]
no_force_exclude: bool,
/// Enable or disable automatic update checks.
#[arg(long, overrides_with("no_update_check"))]
update_check: bool,
#[clap(long, overrides_with("update_check"), hide = true)]
no_update_check: bool,
/// Regular expression matching the name of dummy variables.
#[arg(long)]
pub dummy_variable_rgx: Option<Regex>,
/// The minimum Python version that should be supported.
#[arg(long)]
#[arg(long, help_heading = "Rule configuration")]
pub target_version: Option<PythonVersion>,
/// Set the line-length for length-associated rules and automatic
/// formatting.
#[arg(long)]
#[arg(long, help_heading = "Rule configuration")]
pub line_length: Option<usize>,
/// Regular expression matching the name of dummy variables.
#[arg(long, help_heading = "Rule configuration")]
pub dummy_variable_rgx: Option<Regex>,
/// Disable cache reads.
#[arg(short, long, help_heading = "Miscellaneous")]
pub no_cache: bool,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
pub isolated: bool,
/// Path to the cache directory.
#[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")]
pub cache_dir: Option<PathBuf>,
/// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
/// Exit with status code "0", even upon detecting lint violations.
#[arg(short, long, help_heading = "Miscellaneous")]
pub exit_zero: bool,
/// Enable or disable automatic update checks.
#[arg(
long,
overrides_with("no_update_check"),
help_heading = "Miscellaneous"
)]
update_check: bool,
#[clap(long, overrides_with("update_check"), hide = true)]
no_update_check: bool,
/// Enable automatic additions of `noqa` directives to failing lines.
#[arg(
long,
@@ -151,25 +193,11 @@ pub struct Cli {
conflicts_with = "watch",
)]
pub add_noqa: bool,
/// Clear any caches in the current directory or any subdirectories.
#[arg(
long,
// Fake subcommands.
conflicts_with = "add_noqa",
// conflicts_with = "clean",
conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub clean: bool,
/// Explain a rule.
#[arg(
long,
value_parser=Rule::from_code,
help_heading="Subcommands",
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
@@ -182,6 +210,22 @@ pub struct Cli {
conflicts_with = "watch",
)]
pub explain: Option<&'static Rule>,
/// Clear any caches in the current directory or any subdirectories.
#[arg(
long,
help_heading="Subcommands",
// Fake subcommands.
conflicts_with = "add_noqa",
// conflicts_with = "clean",
conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub clean: bool,
/// Generate shell completion
#[arg(
long,
@@ -229,9 +273,40 @@ pub struct Cli {
conflicts_with = "watch",
)]
pub show_settings: bool,
#[clap(flatten)]
pub log_level_args: LogLevelArgs,
}
impl Cli {
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, clap::Args)]
pub struct LogLevelArgs {
/// Enable verbose logging.
#[arg(short, long, group = "verbosity", help_heading = "Log levels")]
pub verbose: bool,
/// Print lint violations, but nothing else.
#[arg(short, long, group = "verbosity", help_heading = "Log levels")]
pub quiet: bool,
/// Disable all logging (but still exit with status code "1" upon detecting
/// lint violations).
#[arg(short, long, group = "verbosity", help_heading = "Log levels")]
pub silent: bool,
}
impl From<&LogLevelArgs> for LogLevel {
fn from(args: &LogLevelArgs) -> Self {
if args.silent {
LogLevel::Silent
} else if args.quiet {
LogLevel::Quiet
} else if args.verbose {
LogLevel::Verbose
} else {
LogLevel::Default
}
}
}
impl Args {
/// Partition the CLI into command-line arguments and configuration
/// overrides.
pub fn partition(self) -> (Arguments, Overrides) {
@@ -247,12 +322,9 @@ impl Cli {
generate_shell_completion: self.generate_shell_completion,
isolated: self.isolated,
no_cache: self.no_cache,
quiet: self.quiet,
show_files: self.show_files,
show_settings: self.show_settings,
silent: self.silent,
stdin_filename: self.stdin_filename,
verbose: self.verbose,
watch: self.watch,
},
Overrides {
@@ -308,12 +380,9 @@ pub struct Arguments {
pub generate_shell_completion: Option<clap_complete_command::Shell>,
pub isolated: bool,
pub no_cache: bool,
pub quiet: bool,
pub show_files: bool,
pub show_settings: bool,
pub silent: bool,
pub stdin_filename: Option<PathBuf>,
pub verbose: bool,
pub watch: bool,
}
@@ -420,19 +489,6 @@ impl ConfigProcessor for &Overrides {
}
}
/// Map the CLI settings to a `LogLevel`.
pub fn extract_log_level(cli: &Arguments) -> LogLevel {
if cli.silent {
LogLevel::Silent
} else if cli.quiet {
LogLevel::Quiet
} else if cli.verbose {
LogLevel::Verbose
} else {
LogLevel::Default
}
}
/// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`.
pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgnore> {
let mut per_file_ignores: FxHashMap<String, Vec<RuleSelector>> = FxHashMap::default();

View File

@@ -16,15 +16,15 @@ use ruff::linter::add_noqa_to_path;
use ruff::logging::LogLevel;
use ruff::message::{Location, Message};
use ruff::registry::{Linter, Rule, RuleNamespace};
use ruff::resolver::{FileDiscovery, PyprojectDiscovery};
use ruff::resolver::PyprojectDiscovery;
use ruff::settings::flags;
use ruff::settings::types::SerializationFormat;
use ruff::{fix, fs, packaging, resolver, warn_user_once, AutofixAvailability, IOError};
use serde::Serialize;
use walkdir::WalkDir;
use crate::args::Overrides;
use crate::cache;
use crate::cli::Overrides;
use crate::diagnostics::{lint_path, lint_stdin, Diagnostics};
use crate::iterators::par_iter;
@@ -32,15 +32,13 @@ use crate::iterators::par_iter;
pub fn run(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
cache: flags::Cache,
autofix: fix::FixMode,
) -> Result<Diagnostics> {
// Collect all the Python files to check.
let start = Instant::now();
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
@@ -49,9 +47,6 @@ pub fn run(
return Ok(Diagnostics::default());
}
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
// Initialize the cache.
if matches!(cache, flags::Cache::Enabled) {
match &pyproject_strategy {
@@ -156,12 +151,11 @@ fn read_from_stdin() -> Result<String> {
pub fn run_stdin(
filename: Option<&Path>,
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
autofix: fix::FixMode,
) -> Result<Diagnostics> {
if let Some(filename) = filename {
if !resolver::python_file_at_path(filename, pyproject_strategy, file_strategy, overrides)? {
if !resolver::python_file_at_path(filename, pyproject_strategy, overrides)? {
return Ok(Diagnostics::default());
}
}
@@ -182,13 +176,11 @@ pub fn run_stdin(
pub fn add_noqa(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<usize> {
// Collect all the files to check.
let start = Instant::now();
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
@@ -197,9 +189,6 @@ pub fn add_noqa(
return Ok(0);
}
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
let start = Instant::now();
let modifications: usize = par_iter(&paths)
.flatten()
@@ -226,15 +215,10 @@ pub fn add_noqa(
pub fn show_settings(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<()> {
// Collect all files in the hierarchy.
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
// Print the list of files.
let Some(entry) = paths
@@ -255,21 +239,16 @@ pub fn show_settings(
pub fn show_files(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<()> {
// Collect all files in the hierarchy.
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let (paths, _resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
if paths.is_empty() {
warn_user_once!("No Python files found under the given path(s)");
return Ok(());
}
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
// Print the list of files.
for entry in paths
.iter()

View File

@@ -42,9 +42,6 @@ pub fn lint_path(
cache: flags::Cache,
autofix: fix::FixMode,
) -> Result<Diagnostics> {
// Validate the `Settings` and return any errors.
settings.lib.validate()?;
// Check the cache.
// TODO(charlie): `fixer::Mode::Apply` and `fixer::Mode::Diff` both have
// side-effects that aren't captured in the cache. (In practice, it's fine
@@ -116,9 +113,6 @@ pub fn lint_stdin(
settings: &Settings,
autofix: fix::FixMode,
) -> Result<Diagnostics> {
// Validate the `Settings` and return any errors.
settings.validate()?;
// Lint the inputs.
let (messages, fixed) = if matches!(autofix, fix::FixMode::Apply | fix::FixMode::Diff) {
let (transformed, fixed, messages) = lint_fix(

View File

@@ -6,11 +6,11 @@
#![warn(clippy::pedantic)]
#![allow(clippy::must_use_candidate, dead_code)]
mod cli;
mod args;
use clap::CommandFactory;
/// Returns the output of `ruff --help`.
pub fn help() -> String {
cli::Cli::command().render_help().to_string()
args::Args::command().render_help().to_string()
}

View File

@@ -8,90 +8,35 @@
)]
use std::io::{self};
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::mpsc::channel;
use ::ruff::logging::{set_up_logging, LogLevel};
use ::ruff::resolver::{
resolve_settings_with_processor, ConfigProcessor, FileDiscovery, PyprojectDiscovery, Relativity,
};
use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::pyproject;
use ::ruff::resolver::PyprojectDiscovery;
use ::ruff::settings::types::SerializationFormat;
use ::ruff::{fix, fs, warn_user_once};
use anyhow::Result;
use args::Args;
use clap::{CommandFactory, Parser};
use cli::{extract_log_level, Cli, Overrides};
use colored::Colorize;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use path_absolutize::path_dedot;
use printer::{Printer, Violations};
use ruff::settings::{AllSettings, CliSettings};
use ruff::settings::CliSettings;
pub(crate) mod args;
mod cache;
mod cli;
mod commands;
mod diagnostics;
mod iterators;
mod printer;
mod resolve;
#[cfg(all(feature = "update-informer"))]
pub mod updates;
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
fn resolve(
isolated: bool,
config: Option<&Path>,
overrides: &Overrides,
stdin_filename: Option<&Path>,
) -> Result<PyprojectDiscovery> {
if isolated {
// First priority: if we're running in isolated mode, use the default settings.
let mut config = Configuration::default();
overrides.process_config(&mut config);
let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Fixed(settings))
} else if let Some(pyproject) = config {
// Second priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
let settings = resolve_settings_with_processor(pyproject, &Relativity::Cwd, overrides)?;
Ok(PyprojectDiscovery::Fixed(settings))
} else if let Some(pyproject) = pyproject::find_settings_toml(
stdin_filename
.as_ref()
.unwrap_or(&path_dedot::CWD.as_path()),
)? {
// Third priority: find a `pyproject.toml` file in either an ancestor of
// `stdin_filename` (if set) or the current working path all paths relative to
// that directory. (With `Strategy::Hierarchical`, we'll end up finding
// the "closest" `pyproject.toml` file for every Python file later on,
// so these act as the "default" settings.)
let settings = resolve_settings_with_processor(&pyproject, &Relativity::Parent, overrides)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
} else if let Some(pyproject) = pyproject::find_user_settings_toml() {
// Fourth priority: find a user-specific `pyproject.toml`, but resolve all paths
// relative the current working directory. (With `Strategy::Hierarchical`, we'll
// end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.)
let settings = resolve_settings_with_processor(&pyproject, &Relativity::Cwd, overrides)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
} else {
// Fallback: load Ruff's default settings, and resolve all paths relative to the
// current working directory. (With `Strategy::Hierarchical`, we'll end up the
// "closest" `pyproject.toml` file for every Python file later on, so these act
// as the "default" settings.)
let mut config = Configuration::default();
overrides.process_config(&mut config);
let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
}
}
fn inner_main() -> Result<ExitCode> {
// Extract command-line arguments.
let (cli, overrides) = Cli::parse().partition();
let args = Args::parse();
let default_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
@@ -108,11 +53,13 @@ quoting the executed command, along with the relevant file contents and `pyproje
default_panic_hook(info);
}));
let log_level = extract_log_level(&cli);
let log_level: LogLevel = (&args.log_level_args).into();
set_up_logging(&log_level)?;
let (cli, overrides) = args.partition();
if let Some(shell) = cli.generate_shell_completion {
shell.generate(&mut Cli::command(), &mut io::stdout());
shell.generate(&mut Args::command(), &mut io::stdout());
return Ok(ExitCode::SUCCESS);
}
if cli.clean {
@@ -122,31 +69,15 @@ quoting the executed command, along with the relevant file contents and `pyproje
// Construct the "default" settings. These are used when no `pyproject.toml`
// files are present, or files are injected from outside of the hierarchy.
let pyproject_strategy = resolve(
let pyproject_strategy = resolve::resolve(
cli.isolated,
cli.config.as_deref(),
&overrides,
cli.stdin_filename.as_deref(),
)?;
// Validate the `Settings` and return any errors.
match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.lib.validate()?,
PyprojectDiscovery::Hierarchical(settings) => settings.lib.validate()?,
};
// Extract options that are included in `Settings`, but only apply at the top
// level.
let file_strategy = FileDiscovery {
force_exclude: match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.lib.force_exclude,
PyprojectDiscovery::Hierarchical(settings) => settings.lib.force_exclude,
},
respect_gitignore: match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.lib.respect_gitignore,
PyprojectDiscovery::Hierarchical(settings) => settings.lib.respect_gitignore,
},
};
let CliSettings {
fix,
fix_only,
@@ -163,11 +94,11 @@ quoting the executed command, along with the relevant file contents and `pyproje
return Ok(ExitCode::SUCCESS);
}
if cli.show_settings {
commands::show_settings(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
commands::show_settings(&cli.files, &pyproject_strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_files {
commands::show_files(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
commands::show_files(&cli.files, &pyproject_strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
@@ -205,7 +136,13 @@ quoting the executed command, along with the relevant file contents and `pyproje
}
let printer = Printer::new(&format, &log_level, &autofix, &violations);
if cli.watch {
if cli.add_noqa {
let modifications = commands::add_noqa(&cli.files, &pyproject_strategy, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default {
println!("Added {modifications} noqa directives.");
}
} else if cli.watch {
if !matches!(autofix, fix::FixMode::None) {
warn_user_once!("--fix is not enabled in watch mode.");
}
@@ -220,7 +157,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
let messages = commands::run(
&cli.files,
&pyproject_strategy,
&file_strategy,
&overrides,
cache.into(),
fix::FixMode::None,
@@ -250,7 +186,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
let messages = commands::run(
&cli.files,
&pyproject_strategy,
&file_strategy,
&overrides,
cache.into(),
fix::FixMode::None,
@@ -261,12 +196,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
Err(err) => return Err(err.into()),
}
}
} else if cli.add_noqa {
let modifications =
commands::add_noqa(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default {
println!("Added {modifications} noqa directives.");
}
} else {
let is_stdin = cli.files == vec![PathBuf::from("-")];
@@ -275,7 +204,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
commands::run_stdin(
cli.stdin_filename.map(fs::normalize_path).as_deref(),
&pyproject_strategy,
&file_strategy,
&overrides,
autofix,
)?
@@ -283,7 +211,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
commands::run(
&cli.files,
&pyproject_strategy,
&file_strategy,
&overrides,
cache.into(),
autofix,

68
ruff_cli/src/resolve.rs Normal file
View File

@@ -0,0 +1,68 @@
use std::path::Path;
use anyhow::Result;
use path_absolutize::path_dedot;
use ruff::resolver::{
resolve_settings_with_processor, ConfigProcessor, PyprojectDiscovery, Relativity,
};
use ruff::settings::configuration::Configuration;
use ruff::settings::{pyproject, AllSettings};
use crate::args::Overrides;
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
pub fn resolve(
isolated: bool,
config: Option<&Path>,
overrides: &Overrides,
stdin_filename: Option<&Path>,
) -> Result<PyprojectDiscovery> {
// First priority: if we're running in isolated mode, use the default settings.
if isolated {
let mut config = Configuration::default();
overrides.process_config(&mut config);
let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?;
return Ok(PyprojectDiscovery::Fixed(settings));
}
// Second priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
if let Some(pyproject) = config {
let settings = resolve_settings_with_processor(pyproject, &Relativity::Cwd, overrides)?;
return Ok(PyprojectDiscovery::Fixed(settings));
}
// Third priority: find a `pyproject.toml` file in either an ancestor of
// `stdin_filename` (if set) or the current working path all paths relative to
// that directory. (With `Strategy::Hierarchical`, we'll end up finding
// the "closest" `pyproject.toml` file for every Python file later on,
// so these act as the "default" settings.)
if let Some(pyproject) = pyproject::find_settings_toml(
stdin_filename
.as_ref()
.unwrap_or(&path_dedot::CWD.as_path()),
)? {
let settings = resolve_settings_with_processor(&pyproject, &Relativity::Parent, overrides)?;
return Ok(PyprojectDiscovery::Hierarchical(settings));
}
// Fourth priority: find a user-specific `pyproject.toml`, but resolve all paths
// relative the current working directory. (With `Strategy::Hierarchical`, we'll
// end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.)
if let Some(pyproject) = pyproject::find_user_settings_toml() {
let settings = resolve_settings_with_processor(&pyproject, &Relativity::Cwd, overrides)?;
return Ok(PyprojectDiscovery::Hierarchical(settings));
}
// Fallback: load Ruff's default settings, and resolve all paths relative to the
// current working directory. (With `Strategy::Hierarchical`, we'll end up the
// "closest" `pyproject.toml` file for every Python file later on, so these act
// as the "default" settings.)
let mut config = Configuration::default();
overrides.process_config(&mut config);
let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.235"
version = "0.0.236"
edition = "2021"
[dependencies]

View File

@@ -1,29 +1,28 @@
//! Run all code and documentation generation steps.
use anyhow::Result;
use clap::Args;
use crate::{generate_cli_help, generate_json_schema, generate_options, generate_rules_table};
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Write the generated artifacts to stdout (rather than to the filesystem).
#[arg(long)]
dry_run: bool,
}
pub fn main(cli: &Cli) -> Result<()> {
generate_json_schema::main(&generate_json_schema::Cli {
dry_run: cli.dry_run,
pub fn main(args: &Args) -> Result<()> {
generate_json_schema::main(&generate_json_schema::Args {
dry_run: args.dry_run,
})?;
generate_rules_table::main(&generate_rules_table::Cli {
dry_run: cli.dry_run,
generate_rules_table::main(&generate_rules_table::Args {
dry_run: args.dry_run,
})?;
generate_options::main(&generate_options::Cli {
dry_run: cli.dry_run,
generate_options::main(&generate_options::Args {
dry_run: args.dry_run,
})?;
generate_cli_help::main(&generate_cli_help::Cli {
dry_run: cli.dry_run,
generate_cli_help::main(&generate_cli_help::Args {
dry_run: args.dry_run,
})?;
Ok(())
}

View File

@@ -1,15 +1,14 @@
//! Generate CLI help.
use anyhow::Result;
use clap::Args;
use crate::utils::replace_readme_section;
const HELP_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated cli help. -->";
const HELP_END_PRAGMA: &str = "<!-- End auto-generated cli help. -->";
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Write the generated help to stdout (rather than to `README.md`).
#[arg(long)]
pub(crate) dry_run: bool,
@@ -19,10 +18,10 @@ fn trim_lines(s: &str) -> String {
s.lines().map(str::trim_end).collect::<Vec<_>>().join("\n")
}
pub fn main(cli: &Cli) -> Result<()> {
pub fn main(args: &Args) -> Result<()> {
let output = trim_lines(ruff_cli::help().trim());
if cli.dry_run {
if args.dry_run {
print!("{output}");
} else {
replace_readme_section(

View File

@@ -2,22 +2,21 @@ use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use ruff::settings::options::Options;
use schemars::schema_for;
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Write the generated table to stdout (rather than to `ruff.schema.json`).
#[arg(long)]
pub(crate) dry_run: bool,
}
pub fn main(cli: &Cli) -> Result<()> {
pub fn main(args: &Args) -> Result<()> {
let schema = schema_for!(Options);
let schema_string = serde_json::to_string_pretty(&schema).unwrap();
if cli.dry_run {
if args.dry_run {
println!("{schema_string}");
} else {
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))

View File

@@ -1,7 +1,6 @@
//! Generate a Markdown-compatible listing of configuration options.
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::settings::options::Options;
use ruff::settings::options_base::{ConfigurationOptions, OptionEntry, OptionField};
@@ -11,8 +10,8 @@ use crate::utils::replace_readme_section;
const BEGIN_PRAGMA: &str = "<!-- Begin auto-generated options sections. -->";
const END_PRAGMA: &str = "<!-- End auto-generated options sections. -->";
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Write the generated table to stdout (rather than to `README.md`).
#[arg(long)]
pub(crate) dry_run: bool,
@@ -39,7 +38,7 @@ fn emit_field(output: &mut String, field: &OptionField, group_name: Option<&str>
output.push('\n');
}
pub fn main(cli: &Cli) -> Result<()> {
pub fn main(args: &Args) -> Result<()> {
let mut output = String::new();
// Generate all the top-level fields.
@@ -89,7 +88,7 @@ pub fn main(cli: &Cli) -> Result<()> {
}
}
if cli.dry_run {
if args.dry_run {
print!("{output}");
} else {
replace_readme_section(&output, BEGIN_PRAGMA, END_PRAGMA)?;

View File

@@ -1,7 +1,6 @@
//! Generate a Markdown-compatible table of supported lint rules.
use anyhow::Result;
use clap::Args;
use ruff::registry::{Linter, LinterCategory, Rule, RuleNamespace};
use strum::IntoEnumIterator;
@@ -13,8 +12,8 @@ const TABLE_END_PRAGMA: &str = "<!-- End auto-generated sections. -->";
const TOC_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated table of contents. -->";
const TOC_END_PRAGMA: &str = "<!-- End auto-generated table of contents. -->";
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Write the generated table to stdout (rather than to `README.md`).
#[arg(long)]
pub(crate) dry_run: bool,
@@ -43,7 +42,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>)
table_out.push('\n');
}
pub fn main(cli: &Cli) -> Result<()> {
pub fn main(args: &Args) -> Result<()> {
// Generate the table string.
let mut table_out = String::new();
let mut toc_out = String::new();
@@ -96,7 +95,7 @@ pub fn main(cli: &Cli) -> Result<()> {
}
}
if cli.dry_run {
if args.dry_run {
print!("Table of Contents: {toc_out}\n Rules Tables: {table_out}");
} else {
// Extra newline in the markdown numbered list looks weird

View File

@@ -33,45 +33,45 @@ use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
struct Args {
#[command(subcommand)]
command: Commands,
command: Command,
}
#[derive(Subcommand)]
enum Commands {
enum Command {
/// Run all code and documentation generation steps.
GenerateAll(generate_all::Cli),
GenerateAll(generate_all::Args),
/// Generate JSON schema for the TOML configuration file.
GenerateJSONSchema(generate_json_schema::Cli),
GenerateJSONSchema(generate_json_schema::Args),
/// Generate a Markdown-compatible table of supported lint rules.
GenerateRulesTable(generate_rules_table::Cli),
GenerateRulesTable(generate_rules_table::Args),
/// Generate a Markdown-compatible listing of configuration options.
GenerateOptions(generate_options::Cli),
GenerateOptions(generate_options::Args),
/// Generate CLI help.
GenerateCliHelp(generate_cli_help::Cli),
GenerateCliHelp(generate_cli_help::Args),
/// Print the AST for a given Python file.
PrintAST(print_ast::Cli),
PrintAST(print_ast::Args),
/// Print the LibCST CST for a given Python file.
PrintCST(print_cst::Cli),
PrintCST(print_cst::Args),
/// Print the token stream for a given Python file.
PrintTokens(print_tokens::Cli),
PrintTokens(print_tokens::Args),
/// Run round-trip source code generation on a given Python file.
RoundTrip(round_trip::Cli),
RoundTrip(round_trip::Args),
}
fn main() -> Result<()> {
let cli = Cli::parse();
match &cli.command {
Commands::GenerateAll(args) => generate_all::main(args)?,
Commands::GenerateJSONSchema(args) => generate_json_schema::main(args)?,
Commands::GenerateRulesTable(args) => generate_rules_table::main(args)?,
Commands::GenerateOptions(args) => generate_options::main(args)?,
Commands::GenerateCliHelp(args) => generate_cli_help::main(args)?,
Commands::PrintAST(args) => print_ast::main(args)?,
Commands::PrintCST(args) => print_cst::main(args)?,
Commands::PrintTokens(args) => print_tokens::main(args)?,
Commands::RoundTrip(args) => round_trip::main(args)?,
let args = Args::parse();
match &args.command {
Command::GenerateAll(args) => generate_all::main(args)?,
Command::GenerateJSONSchema(args) => generate_json_schema::main(args)?,
Command::GenerateRulesTable(args) => generate_rules_table::main(args)?,
Command::GenerateOptions(args) => generate_options::main(args)?,
Command::GenerateCliHelp(args) => generate_cli_help::main(args)?,
Command::PrintAST(args) => print_ast::main(args)?,
Command::PrintCST(args) => print_cst::main(args)?,
Command::PrintTokens(args) => print_tokens::main(args)?,
Command::RoundTrip(args) => round_trip::main(args)?,
}
Ok(())
}

View File

@@ -4,19 +4,18 @@ use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use rustpython_parser::parser;
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Python file for which to generate the AST.
#[arg(required = true)]
file: PathBuf,
}
pub fn main(cli: &Cli) -> Result<()> {
let contents = fs::read_to_string(&cli.file)?;
let python_ast = parser::parse_program(&contents, &cli.file.to_string_lossy())?;
pub fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
let python_ast = parser::parse_program(&contents, &args.file.to_string_lossy())?;
println!("{python_ast:#?}");
Ok(())
}

View File

@@ -4,17 +4,16 @@ use std::fs;
use std::path::PathBuf;
use anyhow::{bail, Result};
use clap::Args;
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Python file for which to generate the CST.
#[arg(required = true)]
file: PathBuf,
}
pub fn main(cli: &Cli) -> Result<()> {
let contents = fs::read_to_string(&cli.file)?;
pub fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
match libcst_native::parse_module(&contents, None) {
Ok(python_cst) => {
println!("{python_cst:#?}");

View File

@@ -4,18 +4,17 @@ use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use rustpython_parser::lexer;
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Python file for which to generate the AST.
#[arg(required = true)]
file: PathBuf,
}
pub fn main(cli: &Cli) -> Result<()> {
let contents = fs::read_to_string(&cli.file)?;
pub fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
for (_, tok, _) in lexer::make_tokenizer(&contents).flatten() {
println!("{tok:#?}");
}

View File

@@ -4,18 +4,17 @@ use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use ruff::source_code::round_trip;
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Python file to round-trip.
#[arg(required = true)]
file: PathBuf,
}
pub fn main(cli: &Cli) -> Result<()> {
let contents = fs::read_to_string(&cli.file)?;
println!("{}", round_trip(&contents, &cli.file.to_string_lossy())?);
pub fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
println!("{}", round_trip(&contents, &args.file.to_string_lossy())?);
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.235"
version = "0.0.236"
edition = "2021"
[lib]

View File

@@ -1,3 +1,4 @@
use std::cmp::Reverse;
use std::collections::HashSet;
use proc_macro2::{Ident, Span};
@@ -70,7 +71,7 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
});
}
parsed.sort_by_key(|(prefix, _)| prefix.len());
parsed.sort_by_key(|(prefix, _)| Reverse(prefix.len()));
let mut if_statements = quote!();
let mut into_iter_match_arms = quote!();

View File

@@ -84,7 +84,7 @@ mod tests {
fp.write("\n")
elif line.strip() == "/// Ruff-specific rules":
fp.write(f"/// [{plugin}]({url})\n")
fp.write(f"{indent}/// [{plugin}]({url})\n")
fp.write(f'{indent}#[prefix = "{prefix_code}"]\n')
fp.write(f"{indent}{pascal_case(plugin)},")
fp.write("\n")

View File

@@ -489,6 +489,17 @@ pub fn is_const_none(expr: &Expr) -> bool {
)
}
/// Return `true` if an [`Expr`] is `True`.
pub fn is_const_true(expr: &Expr) -> bool {
matches!(
&expr.node,
ExprKind::Constant {
value: Constant::Bool(true),
kind: None
},
)
}
/// Return `true` if a keyword argument is present with a non-`None` value.
pub fn has_non_none_keyword(keywords: &[Keyword], keyword: &str) -> bool {
find_keyword(keywords, keyword).map_or(false, |keyword| {
@@ -665,6 +676,24 @@ pub fn match_trailing_content(stmt: &Stmt, locator: &Locator) -> bool {
false
}
/// If a `Stmt` has a trailing comment, return the index of the hash.
pub fn match_trailing_comment(stmt: &Stmt, locator: &Locator) -> Option<usize> {
let range = Range::new(
stmt.end_location.unwrap(),
Location::new(stmt.end_location.unwrap().row() + 1, 0),
);
let suffix = locator.slice_source_code_range(&range);
for (i, char) in suffix.chars().enumerate() {
if char == '#' {
return Some(i);
}
if !char.is_whitespace() {
return None;
}
}
None
}
/// Return the number of trailing empty lines following a statement.
pub fn count_trailing_lines(stmt: &Stmt, locator: &Locator) -> usize {
let suffix =
@@ -974,6 +1003,21 @@ impl<'a> SimpleCallArgs<'a> {
}
}
/// Return `true` if the given `Expr` is a potential logging call. Matches
/// `logging.error`, `logger.error`, `self.logger.error`, etc., but not
/// arbitrary `foo.error` calls.
pub fn is_logger_candidate(func: &Expr) -> bool {
if let ExprKind::Attribute { value, .. } = &func.node {
let call_path = collect_call_path(value);
if let Some(tail) = call_path.last() {
if *tail == "logging" || tail.ends_with("logger") {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use anyhow::Result;

View File

@@ -89,9 +89,6 @@ pub struct Scope<'a> {
pub uses_locals: bool,
/// A map from bound name to binding index.
pub values: FxHashMap<&'a str, usize>,
/// A list of (name, index) pairs for bindings that were overridden in the
/// scope.
pub overridden: Vec<(&'a str, usize)>,
}
impl<'a> Scope<'a> {
@@ -102,11 +99,27 @@ impl<'a> Scope<'a> {
import_starred: false,
uses_locals: false,
values: FxHashMap::default(),
overridden: Vec::new(),
}
}
}
// Pyflakes defines the following binding hierarchy (via inheritance):
// Binding
// ExportBinding
// Annotation
// Argument
// Assignment
// NamedExprAssignment
// Definition
// FunctionDefinition
// ClassDefinition
// Builtin
// Importation
// SubmoduleImportation
// ImportationFrom
// StarImportation
// FutureImportation
#[derive(Clone, Debug)]
pub enum BindingKind<'a> {
Annotation,
@@ -127,10 +140,12 @@ pub enum BindingKind<'a> {
SubmoduleImportation(&'a str, &'a str),
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct Binding<'a> {
pub kind: BindingKind<'a>,
pub range: Range,
/// The context in which the binding was created.
pub context: ExecutionContext,
/// The statement in which the [`Binding`] was defined.
pub source: Option<RefEquality<'a, Stmt>>,
/// Tuple of (scope index, range) indicating the scope and range at which
@@ -147,33 +162,16 @@ pub struct Binding<'a> {
}
#[derive(Copy, Clone)]
pub enum UsageContext {
pub enum ExecutionContext {
Runtime,
Typing,
}
// Pyflakes defines the following binding hierarchy (via inheritance):
// Binding
// ExportBinding
// Annotation
// Argument
// Assignment
// NamedExprAssignment
// Definition
// FunctionDefinition
// ClassDefinition
// Builtin
// Importation
// SubmoduleImportation
// ImportationFrom
// StarImportation
// FutureImportation
impl<'a> Binding<'a> {
pub fn mark_used(&mut self, scope: usize, range: Range, context: UsageContext) {
pub fn mark_used(&mut self, scope: usize, range: Range, context: ExecutionContext) {
match context {
UsageContext::Runtime => self.runtime_usage = Some((scope, range)),
UsageContext::Typing => self.typing_usage = Some((scope, range)),
ExecutionContext::Runtime => self.runtime_usage = Some((scope, range)),
ExecutionContext::Typing => self.typing_usage = Some((scope, range)),
}
}

View File

@@ -12,7 +12,7 @@ use crate::ast::whitespace::LinesWithTrailingNewline;
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_module;
use crate::fix::Fix;
use crate::source_code::{Indexer, Locator};
use crate::source_code::{Indexer, Locator, Stylist};
/// Determine if a body contains only a single statement, taking into account
/// deleted.
@@ -157,6 +157,7 @@ pub fn delete_stmt(
deleted: &[&Stmt],
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Result<Fix> {
if parent
.map(|parent| is_lone_child(stmt, parent, deleted))
@@ -179,7 +180,11 @@ pub fn delete_stmt(
} else if helpers::preceded_by_continuation(stmt, indexer) {
if is_end_of_file(stmt, locator) && stmt.location.column() == 0 {
// Special-case: a file can't end in a continuation.
Fix::replacement("\n".to_string(), stmt.location, stmt.end_location.unwrap())
Fix::replacement(
stylist.line_ending().to_string(),
stmt.location,
stmt.end_location.unwrap(),
)
} else {
Fix::deletion(stmt.location, stmt.end_location.unwrap())
}
@@ -200,6 +205,7 @@ pub fn remove_unused_imports<'a>(
deleted: &[&Stmt],
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let mut tree = match_module(module_text)?;
@@ -237,7 +243,7 @@ pub fn remove_unused_imports<'a>(
if !found_star {
bail!("Expected \'*\' for unused import");
}
return delete_stmt(stmt, parent, deleted, locator, indexer);
return delete_stmt(stmt, parent, deleted, locator, indexer, stylist);
} else {
bail!("Expected: ImportNames::Aliases | ImportNames::Star");
}
@@ -298,9 +304,13 @@ pub fn remove_unused_imports<'a>(
}
if aliases.is_empty() {
delete_stmt(stmt, parent, deleted, locator, indexer)
delete_stmt(stmt, parent, deleted, locator, indexer, stylist)
} else {
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(

View File

@@ -1,5 +1,6 @@
//! Lint rules based on AST traversal.
use std::iter;
use std::path::Path;
use itertools::Itertools;
@@ -19,8 +20,8 @@ use crate::ast::helpers::{binding_range, collect_call_path, extract_handler_name
use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
Binding, BindingKind, CallPath, ClassDef, FunctionDef, Lambda, Node, Range, RefEquality, Scope,
ScopeKind, UsageContext,
Binding, BindingKind, CallPath, ClassDef, ExecutionContext, FunctionDef, Lambda, Node, Range,
RefEquality, Scope, ScopeKind,
};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{branch_detection, cast, helpers, operations, visitor};
@@ -34,10 +35,11 @@ use crate::registry::{Diagnostic, Rule};
use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_pie, flake8_print,
flake8_pytest_style, flake8_return, flake8_simplify, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle,
pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format,
flake8_pie, flake8_print, flake8_pytest_style, flake8_return, flake8_simplify,
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, flake8_use_pathlib, mccabe,
pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade,
ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
@@ -79,7 +81,7 @@ pub struct Checker<'a> {
pub(crate) exprs: Vec<RefEquality<'a, Expr>>,
pub(crate) scopes: Vec<Scope<'a>>,
pub(crate) scope_stack: Vec<usize>,
pub(crate) dead_scopes: Vec<usize>,
pub(crate) dead_scopes: Vec<(usize, Vec<usize>)>,
deferred_string_type_definitions: Vec<(Range, &'a str, bool, DeferralContext<'a>)>,
deferred_type_definitions: Vec<(&'a Expr, bool, DeferralContext<'a>)>,
deferred_functions: Vec<(&'a Stmt, DeferralContext<'a>, VisibleScope)>,
@@ -100,7 +102,6 @@ pub struct Checker<'a> {
except_handlers: Vec<Vec<Vec<&'a str>>>,
// Check-specific state.
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
pub(crate) type_checking_blocks: Vec<&'a Stmt>,
}
impl<'a> Checker<'a> {
@@ -161,7 +162,6 @@ impl<'a> Checker<'a> {
except_handlers: vec![],
// Check-specific state.
flake8_bugbear_seen: vec![],
type_checking_blocks: vec![],
}
}
@@ -330,6 +330,7 @@ where
let ranges = helpers::find_names(stmt, self.locator);
if scope_index != GLOBAL_SCOPE_INDEX {
// Add the binding to the current scope.
let context = self.execution_context();
let scope = &mut self.scopes[scope_index];
let usage = Some((scope.id, Range::from_located(stmt)));
for (name, range) in names.iter().zip(ranges.iter()) {
@@ -341,6 +342,7 @@ where
typing_usage: None,
range: *range,
source: Some(RefEquality(stmt)),
context,
});
scope.values.insert(name, index);
}
@@ -357,6 +359,7 @@ where
let scope_index = *self.scope_stack.last().expect("No current scope found");
let ranges = helpers::find_names(stmt, self.locator);
if scope_index != GLOBAL_SCOPE_INDEX {
let context = self.execution_context();
let scope = &mut self.scopes[scope_index];
let usage = Some((scope.id, Range::from_located(stmt)));
for (name, range) in names.iter().zip(ranges.iter()) {
@@ -369,6 +372,7 @@ where
typing_usage: None,
range: *range,
source: Some(RefEquality(stmt)),
context,
});
scope.values.insert(name, index);
}
@@ -674,6 +678,8 @@ where
for expr in &args.defaults {
self.visit_expr(expr);
}
let context = self.execution_context();
self.add_binding(
name,
Binding {
@@ -683,6 +689,7 @@ where
typing_usage: None,
range: Range::from_located(stmt),
source: Some(self.current_stmt().clone()),
context,
},
);
}
@@ -837,6 +844,7 @@ where
typing_usage: None,
range: Range::from_located(alias),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
} else {
@@ -876,6 +884,7 @@ where
typing_usage: None,
range: Range::from_located(alias),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
}
@@ -1118,6 +1127,7 @@ where
typing_usage: None,
range: Range::from_located(alias),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
@@ -1154,6 +1164,7 @@ where
typing_usage: None,
range: Range::from_located(stmt),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
@@ -1228,6 +1239,7 @@ where
typing_usage: None,
range,
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
}
@@ -1394,6 +1406,16 @@ where
pyupgrade::rules::os_error_alias(self, &item);
}
}
if self.settings.rules.enabled(&Rule::RaiseVanillaClass) {
if let Some(expr) = exc {
tryceratops::rules::raise_vanilla_class(self, expr);
}
}
if self.settings.rules.enabled(&Rule::RaiseVanillaArgs) {
if let Some(expr) = exc {
tryceratops::rules::raise_vanilla_args(self, expr);
}
}
}
StmtKind::AugAssign { target, .. } => {
self.handle_node_load(target);
@@ -1607,6 +1629,9 @@ where
if self.settings.rules.enabled(&Rule::RaiseWithinTry) {
tryceratops::rules::raise_within_try(self, body);
}
if self.settings.rules.enabled(&Rule::ErrorInsteadOfException) {
tryceratops::rules::error_instead_of_exception(self, handlers);
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
@@ -1736,6 +1761,7 @@ where
typing_usage: None,
range: Range::from_located(stmt),
source: Some(RefEquality(stmt)),
context: self.execution_context(),
});
self.scopes[GLOBAL_SCOPE_INDEX].values.insert(name, index);
}
@@ -1799,6 +1825,7 @@ where
typing_usage: None,
range: Range::from_located(stmt),
source: Some(RefEquality(stmt)),
context: self.execution_context(),
});
self.scopes[GLOBAL_SCOPE_INDEX].values.insert(name, index);
}
@@ -1862,7 +1889,6 @@ where
if self.settings.rules.enabled(&Rule::EmptyTypeCheckingBlock) {
flake8_type_checking::rules::empty_type_checking_block(self, test, body);
}
self.type_checking_blocks.push(stmt);
let prev_in_type_checking_block = self.in_type_checking_block;
self.in_type_checking_block = true;
@@ -1894,6 +1920,7 @@ where
typing_usage: None,
range: Range::from_located(stmt),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
}
@@ -2642,6 +2669,19 @@ where
{
flake8_use_pathlib::helpers::replaceable_by_pathlib(self, func);
}
// flake8-logging-format
if self.settings.rules.enabled(&Rule::LoggingStringFormat)
|| self.settings.rules.enabled(&Rule::LoggingPercentFormat)
|| self.settings.rules.enabled(&Rule::LoggingStringConcat)
|| self.settings.rules.enabled(&Rule::LoggingFString)
|| self.settings.rules.enabled(&Rule::LoggingWarn)
|| self.settings.rules.enabled(&Rule::LoggingExtraAttrClash)
|| self.settings.rules.enabled(&Rule::LoggingExcInfo)
|| self.settings.rules.enabled(&Rule::LoggingRedundantExcInfo)
{
flake8_logging_format::rules::logging_call(self, func, args, keywords);
}
}
ExprKind::Dict { keys, values } => {
if self
@@ -3584,6 +3624,7 @@ where
typing_usage: None,
range: Range::from_located(arg),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
@@ -3660,11 +3701,12 @@ impl<'a> Checker<'a> {
}
fn pop_scope(&mut self) {
self.dead_scopes.push(
self.dead_scopes.push((
self.scope_stack
.pop()
.expect("Attempted to pop without scope"),
);
self.scope_stack.clone(),
));
}
fn bind_builtins(&mut self) {
@@ -3684,6 +3726,7 @@ impl<'a> Checker<'a> {
synthetic_usage: None,
typing_usage: None,
source: None,
context: ExecutionContext::Runtime,
});
scope.values.insert(builtin, index);
}
@@ -3720,6 +3763,18 @@ impl<'a> Checker<'a> {
.map(|index| &self.scopes[*index])
}
pub fn execution_context(&self) -> ExecutionContext {
if self.in_type_checking_block
|| self.in_annotation
|| self.in_deferred_string_type_definition
|| self.in_deferred_type_definition
{
ExecutionContext::Typing
} else {
ExecutionContext::Runtime
}
}
fn add_binding<'b>(&mut self, name: &'b str, binding: Binding<'a>)
where
'b: 'a,
@@ -3837,17 +3892,8 @@ impl<'a> Checker<'a> {
}
if let Some(index) = scope.values.get(&id.as_str()) {
let context = if self.in_type_checking_block
|| self.in_annotation
|| self.in_deferred_string_type_definition
|| self.in_deferred_type_definition
{
UsageContext::Typing
} else {
UsageContext::Runtime
};
// Mark the binding as used.
let context = self.execution_context();
self.bindings[*index].mark_used(scope_id, Range::from_located(expr), context);
if matches!(self.bindings[*index].kind, BindingKind::Annotation)
@@ -4030,6 +4076,7 @@ impl<'a> Checker<'a> {
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
return;
@@ -4049,6 +4096,7 @@ impl<'a> Checker<'a> {
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
return;
@@ -4064,6 +4112,7 @@ impl<'a> Checker<'a> {
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
return;
@@ -4116,6 +4165,7 @@ impl<'a> Checker<'a> {
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
return;
@@ -4131,6 +4181,7 @@ impl<'a> Checker<'a> {
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
}
@@ -4336,13 +4387,48 @@ impl<'a> Checker<'a> {
return;
}
let mut diagnostics: Vec<Diagnostic> = vec![];
for scope in self
.dead_scopes
.iter()
.rev()
.map(|index| &self.scopes[*index])
// Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let runtime_imports: Vec<Vec<&Binding>> = if !self.settings.flake8_type_checking.strict
&& (self
.settings
.rules
.enabled(&Rule::RuntimeImportInTypeCheckingBlock)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyFirstPartyImport)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyThirdPartyImport)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyStandardLibraryImport))
{
self.scopes
.iter()
.map(|scope| {
scope
.values
.values()
.map(|index| &self.bindings[*index])
.filter(|binding| {
flake8_type_checking::helpers::is_valid_runtime_import(binding)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
} else {
vec![]
};
let mut diagnostics: Vec<Diagnostic> = vec![];
for (index, stack) in self.dead_scopes.iter().rev() {
let scope = &self.scopes[*index];
// PLW0602
if self
.settings
@@ -4481,25 +4567,28 @@ impl<'a> Checker<'a> {
.rules
.enabled(&Rule::TypingOnlyStandardLibraryImport)
{
for (.., index) in scope
.values
.iter()
.chain(scope.overridden.iter().map(|(a, b)| (a, b)))
{
let runtime_imports: Vec<&Binding> = if self.settings.flake8_type_checking.strict {
vec![]
} else {
stack
.iter()
.chain(iter::once(index))
.flat_map(|index| runtime_imports[*index].iter())
.copied()
.collect()
};
for (.., index) in &scope.values {
let binding = &self.bindings[*index];
if let Some(diagnostic) =
flake8_type_checking::rules::runtime_import_in_type_checking_block(
binding,
&self.type_checking_blocks,
)
flake8_type_checking::rules::runtime_import_in_type_checking_block(binding)
{
diagnostics.push(diagnostic);
}
if let Some(diagnostic) =
flake8_type_checking::rules::typing_only_runtime_import(
binding,
&self.type_checking_blocks,
&runtime_imports,
self.package,
self.settings,
)
@@ -4522,11 +4611,7 @@ impl<'a> Checker<'a> {
let mut ignored: FxHashMap<BindingContext, Vec<UnusedImport>> =
FxHashMap::default();
for (name, index) in scope
.values
.iter()
.chain(scope.overridden.iter().map(|(a, b)| (a, b)))
{
for (name, index) in &scope.values {
let binding = &self.bindings[*index];
let full_name = match &binding.kind {
@@ -4598,6 +4683,7 @@ impl<'a> Checker<'a> {
&deleted,
self.locator,
self.indexer,
self.stylist,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {

View File

@@ -32,9 +32,6 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Diagnosti
// Load the relevant `Settings` for the given `Path`.
let settings = resolve(path)?;
// Validate the `Settings` and return any errors.
settings.validate()?;
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);

View File

@@ -11,8 +11,8 @@ use crate::registry::Rule;
use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_errmsg,
flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes,
flake8_tidy_imports, flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle,
pydocstyle, pylint, pyupgrade,
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming,
pycodestyle, pydocstyle, pylint, pyupgrade,
};
use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
@@ -141,13 +141,14 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()),
flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()),
flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()),
flake8_tidy_imports: Some(flake8_tidy_imports::Settings::default().into()),
flake8_implicit_str_concat: Some(
flake8_implicit_str_concat::settings::Settings::default().into(),
),
flake8_import_conventions: Some(
flake8_import_conventions::settings::Settings::default().into(),
),
flake8_tidy_imports: Some(flake8_tidy_imports::Settings::default().into()),
flake8_type_checking: Some(flake8_type_checking::settings::Settings::default().into()),
flake8_unused_arguments: Some(
flake8_unused_arguments::settings::Settings::default().into(),
),

View File

@@ -42,9 +42,6 @@ pub fn check_path(
autofix: flags::Autofix,
noqa: flags::Noqa,
) -> Result<Vec<Diagnostic>> {
// Validate the `Settings` and return any errors.
settings.validate()?;
// Aggregate all diagnostics.
let mut diagnostics: Vec<Diagnostic> = vec![];
@@ -187,9 +184,6 @@ const MAX_ITERATIONS: usize = 100;
/// Add any missing `#noqa` pragmas to the source code at the given `Path`.
pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
// Validate the `Settings` and return any errors.
settings.validate()?;
// Read the file from disk.
let contents = fs::read_file(path)?;

View File

@@ -211,6 +211,7 @@ mod tests {
use crate::ast::types::Range;
use crate::noqa::{add_noqa_inner, NOQA_LINE_REGEX};
use crate::registry::Diagnostic;
use crate::rules::pycodestyle::rules::AmbiguousVariableName;
use crate::settings::hashable::HashableHashSet;
use crate::source_code::LineEnding;
use crate::violations;
@@ -264,7 +265,7 @@ mod tests {
let diagnostics = vec![
Diagnostic::new(
violations::AmbiguousVariableName("x".to_string()),
AmbiguousVariableName("x".to_string()),
Range::new(Location::new(1, 0), Location::new(1, 0)),
),
Diagnostic::new(
@@ -287,7 +288,7 @@ mod tests {
let diagnostics = vec![
Diagnostic::new(
violations::AmbiguousVariableName("x".to_string()),
AmbiguousVariableName("x".to_string()),
Range::new(Location::new(1, 0), Location::new(1, 0)),
),
Diagnostic::new(

View File

@@ -13,3 +13,12 @@ pub fn is_identifier(s: &str) -> bool {
// Are the rest of the characters letters, digits, or underscores?
s.chars().skip(1).all(|c| c.is_alphanumeric() || c == '_')
}
/// Returns `true` if a string is a private identifier, such that, when the
/// identifier is defined in a class definition, it will be mangled prior to
/// code generation.
///
/// See: <https://docs.python.org/3.5/reference/expressions.html?highlight=mangling#index-5>.
pub fn is_mangled_private(id: &str) -> bool {
id.starts_with("__") && !id.ends_with("__")
}

View File

@@ -13,26 +13,26 @@ use crate::{rules, violations};
ruff_macros::define_rule_mapping!(
// pycodestyle errors
E101 => violations::MixedSpacesAndTabs,
E101 => rules::pycodestyle::rules::MixedSpacesAndTabs,
E401 => violations::MultipleImportsOnOneLine,
E402 => violations::ModuleImportNotAtTopOfFile,
E501 => violations::LineTooLong,
E711 => violations::NoneComparison,
E712 => violations::TrueFalseComparison,
E713 => violations::NotInTest,
E714 => violations::NotIsTest,
E721 => violations::TypeComparison,
E722 => violations::DoNotUseBareExcept,
E731 => violations::DoNotAssignLambda,
E741 => violations::AmbiguousVariableName,
E742 => violations::AmbiguousClassName,
E743 => violations::AmbiguousFunctionName,
E501 => rules::pycodestyle::rules::LineTooLong,
E711 => rules::pycodestyle::rules::NoneComparison,
E712 => rules::pycodestyle::rules::TrueFalseComparison,
E713 => rules::pycodestyle::rules::NotInTest,
E714 => rules::pycodestyle::rules::NotIsTest,
E721 => rules::pycodestyle::rules::TypeComparison,
E722 => rules::pycodestyle::rules::DoNotUseBareExcept,
E731 => rules::pycodestyle::rules::DoNotAssignLambda,
E741 => rules::pycodestyle::rules::AmbiguousVariableName,
E742 => rules::pycodestyle::rules::AmbiguousClassName,
E743 => rules::pycodestyle::rules::AmbiguousFunctionName,
E902 => violations::IOError,
E999 => violations::SyntaxError,
// pycodestyle warnings
W292 => violations::NoNewLineAtEndOfFile,
W505 => violations::DocLineTooLong,
W605 => violations::InvalidEscapeSequence,
W292 => rules::pycodestyle::rules::NoNewLineAtEndOfFile,
W505 => rules::pycodestyle::rules::DocLineTooLong,
W605 => rules::pycodestyle::rules::InvalidEscapeSequence,
// pyflakes
F401 => violations::UnusedImport,
F402 => violations::ImportShadowedByLoopVar,
@@ -318,10 +318,10 @@ ruff_macros::define_rule_mapping!(
N817 => violations::CamelcaseImportedAsAcronym,
N818 => violations::ErrorSuffixOnExceptionName,
// isort
I001 => violations::UnsortedImports,
I002 => violations::MissingRequiredImport,
I001 => rules::isort::rules::UnsortedImports,
I002 => rules::isort::rules::MissingRequiredImport,
// eradicate
ERA001 => violations::CommentedOutCode,
ERA001 => rules::eradicate::rules::CommentedOutCode,
// flake8-bandit
S101 => violations::AssertUsed,
S102 => violations::ExecUsed,
@@ -340,9 +340,9 @@ ruff_macros::define_rule_mapping!(
S612 => rules::flake8_bandit::rules::LoggingConfigInsecureListen,
S701 => violations::Jinja2AutoescapeFalse,
// flake8-boolean-trap
FBT001 => violations::BooleanPositionalArgInFunctionDefinition,
FBT002 => violations::BooleanDefaultValueInFunctionDefinition,
FBT003 => violations::BooleanPositionalValueInFunctionCall,
FBT001 => rules::flake8_boolean_trap::rules::BooleanPositionalArgInFunctionDefinition,
FBT002 => rules::flake8_boolean_trap::rules::BooleanDefaultValueInFunctionDefinition,
FBT003 => rules::flake8_boolean_trap::rules::BooleanPositionalValueInFunctionCall,
// flake8-unused-arguments
ARG001 => violations::UnusedFunctionArgument,
ARG002 => violations::UnusedMethodArgument,
@@ -350,7 +350,7 @@ ruff_macros::define_rule_mapping!(
ARG004 => violations::UnusedStaticMethodArgument,
ARG005 => violations::UnusedLambdaArgument,
// flake8-import-conventions
ICN001 => violations::ImportAliasIsNotConventional,
ICN001 => rules::flake8_import_conventions::rules::ImportAliasIsNotConventional,
// flake8-datetimez
DTZ001 => violations::CallDatetimeWithoutTzinfo,
DTZ002 => violations::CallDatetimeToday,
@@ -410,18 +410,18 @@ ruff_macros::define_rule_mapping!(
PT025 => violations::ErroneousUseFixturesOnFixture,
PT026 => violations::UseFixturesWithoutParameters,
// flake8-pie
PIE790 => violations::NoUnnecessaryPass,
PIE794 => violations::DupeClassFieldDefinitions,
PIE796 => violations::PreferUniqueEnums,
PIE800 => violations::NoUnnecessarySpread,
PIE804 => violations::NoUnnecessaryDictKwargs,
PIE807 => violations::PreferListBuiltin,
PIE790 => rules::flake8_pie::rules::NoUnnecessaryPass,
PIE794 => rules::flake8_pie::rules::DupeClassFieldDefinitions,
PIE796 => rules::flake8_pie::rules::PreferUniqueEnums,
PIE800 => rules::flake8_pie::rules::NoUnnecessarySpread,
PIE804 => rules::flake8_pie::rules::NoUnnecessaryDictKwargs,
PIE807 => rules::flake8_pie::rules::PreferListBuiltin,
// flake8-commas
COM812 => violations::TrailingCommaMissing,
COM818 => violations::TrailingCommaOnBareTupleProhibited,
COM819 => violations::TrailingCommaProhibited,
COM812 => rules::flake8_commas::rules::TrailingCommaMissing,
COM818 => rules::flake8_commas::rules::TrailingCommaOnBareTupleProhibited,
COM819 => rules::flake8_commas::rules::TrailingCommaProhibited,
// flake8-no-pep420
INP001 => violations::ImplicitNamespacePackage,
INP001 => rules::flake8_no_pep420::rules::ImplicitNamespacePackage,
// flake8-executable
EXE001 => rules::flake8_executable::rules::ShebangNotExecutable,
EXE002 => rules::flake8_executable::rules::ShebangMissingExecutableFile,
@@ -435,11 +435,14 @@ ruff_macros::define_rule_mapping!(
TCH004 => rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock,
TCH005 => rules::flake8_type_checking::rules::EmptyTypeCheckingBlock,
// tryceratops
TRY002 => rules::tryceratops::rules::RaiseVanillaClass,
TRY003 => rules::tryceratops::rules::RaiseVanillaArgs,
TRY004 => rules::tryceratops::rules::PreferTypeError,
TRY200 => rules::tryceratops::rules::ReraiseNoCause,
TRY201 => rules::tryceratops::rules::VerboseRaise,
TRY300 => rules::tryceratops::rules::TryConsiderElse,
TRY301 => rules::tryceratops::rules::RaiseWithinTry,
TRY400 => rules::tryceratops::rules::ErrorInsteadOfException,
// flake8-use-pathlib
PTH100 => rules::flake8_use_pathlib::violations::PathlibAbspath,
PTH101 => rules::flake8_use_pathlib::violations::PathlibChmod,
@@ -466,6 +469,15 @@ ruff_macros::define_rule_mapping!(
PTH122 => rules::flake8_use_pathlib::violations::PathlibSplitext,
PTH123 => rules::flake8_use_pathlib::violations::PathlibOpen,
PTH124 => rules::flake8_use_pathlib::violations::PathlibPyPath,
// flake8-logging-format
G001 => rules::flake8_logging_format::violations::LoggingStringFormat,
G002 => rules::flake8_logging_format::violations::LoggingPercentFormat,
G003 => rules::flake8_logging_format::violations::LoggingStringConcat,
G004 => rules::flake8_logging_format::violations::LoggingFString,
G010 => rules::flake8_logging_format::violations::LoggingWarn,
G101 => rules::flake8_logging_format::violations::LoggingExtraAttrClash,
G201 => rules::flake8_logging_format::violations::LoggingExcInfo,
G202 => rules::flake8_logging_format::violations::LoggingRedundantExcInfo,
// ruff
RUF001 => violations::AmbiguousUnicodeCharacterString,
RUF002 => violations::AmbiguousUnicodeCharacterDocstring,
@@ -490,15 +502,15 @@ pub enum Linter {
/// [isort](https://pypi.org/project/isort/)
#[prefix = "I"]
Isort,
/// [pep8-naming](https://pypi.org/project/pep8-naming/)
#[prefix = "N"]
PEP8Naming,
/// [pydocstyle](https://pypi.org/project/pydocstyle/)
#[prefix = "D"]
Pydocstyle,
/// [pyupgrade](https://pypi.org/project/pyupgrade/)
#[prefix = "UP"]
Pyupgrade,
/// [pep8-naming](https://pypi.org/project/pep8-naming/)
#[prefix = "N"]
PEP8Naming,
/// [flake8-2020](https://pypi.org/project/flake8-2020/)
#[prefix = "YTT"]
Flake82020,
@@ -520,21 +532,39 @@ pub enum Linter {
/// [flake8-builtins](https://pypi.org/project/flake8-builtins/)
#[prefix = "A"]
Flake8Builtins,
/// [flake8-commas](https://pypi.org/project/flake8-commas/)
#[prefix = "COM"]
Flake8Commas,
/// [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
#[prefix = "C4"]
Flake8Comprehensions,
/// [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
#[prefix = "DTZ"]
Flake8Datetimez,
/// [flake8-debugger](https://pypi.org/project/flake8-debugger/)
#[prefix = "T10"]
Flake8Debugger,
/// [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
#[prefix = "EM"]
Flake8ErrMsg,
/// [flake8-executable](https://pypi.org/project/flake8-executable/)
#[prefix = "EXE"]
Flake8Executable,
/// [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
#[prefix = "ISC"]
Flake8ImplicitStrConcat,
/// [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions)
#[prefix = "ICN"]
Flake8ImportConventions,
/// [flake8-logging-format](https://pypi.org/project/flake8-logging-format/0.9.0/)
#[prefix = "G"]
Flake8LoggingFormat,
/// [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/)
#[prefix = "INP"]
Flake8NoPep420,
/// [flake8-pie](https://pypi.org/project/flake8-pie/)
#[prefix = "PIE"]
Flake8Pie,
/// [flake8-print](https://pypi.org/project/flake8-print/)
#[prefix = "T20"]
Flake8Print,
@@ -553,12 +583,15 @@ pub enum Linter {
/// [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
#[prefix = "TID"]
Flake8TidyImports,
/// [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
#[prefix = "TCH"]
Flake8TypeChecking,
/// [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/)
#[prefix = "ARG"]
Flake8UnusedArguments,
/// [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
#[prefix = "DTZ"]
Flake8Datetimez,
/// [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
#[prefix = "PTH"]
Flake8UsePathlib,
/// [eradicate](https://pypi.org/project/eradicate/)
#[prefix = "ERA"]
Eradicate,
@@ -571,27 +604,9 @@ pub enum Linter {
/// [Pylint](https://pypi.org/project/pylint/)
#[prefix = "PL"]
Pylint,
/// [flake8-pie](https://pypi.org/project/flake8-pie/)
#[prefix = "PIE"]
Flake8Pie,
/// [flake8-commas](https://pypi.org/project/flake8-commas/)
#[prefix = "COM"]
Flake8Commas,
/// [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/)
#[prefix = "INP"]
Flake8NoPep420,
/// [flake8-executable](https://pypi.org/project/flake8-executable/)
#[prefix = "EXE"]
Flake8Executable,
/// [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
#[prefix = "TCH"]
Flake8TypeChecking,
/// [tryceratops](https://pypi.org/project/tryceratops/1.1.0/)
#[prefix = "TRY"]
Tryceratops,
/// [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
#[prefix = "PTH"]
Flake8UsePathlib,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,

View File

@@ -16,13 +16,6 @@ use crate::settings::configuration::Configuration;
use crate::settings::pyproject::settings_toml;
use crate::settings::{pyproject, AllSettings, Settings};
/// The strategy used to discover Python files in the filesystem..
#[derive(Debug)]
pub struct FileDiscovery {
pub force_exclude: bool,
pub respect_gitignore: bool,
}
/// The strategy used to discover the relevant `pyproject.toml` file for each
/// Python file.
#[derive(Debug)]
@@ -35,6 +28,15 @@ pub enum PyprojectDiscovery {
Hierarchical(AllSettings),
}
impl PyprojectDiscovery {
fn top_level_settings(&self) -> &AllSettings {
match self {
PyprojectDiscovery::Fixed(settings) => settings,
PyprojectDiscovery::Hierarchical(settings) => settings,
}
}
}
/// The strategy for resolving file paths in a `pyproject.toml`.
pub enum Relativity {
/// Resolve file paths relative to the current working directory.
@@ -98,26 +100,6 @@ impl Resolver {
pub fn iter(&self) -> impl Iterator<Item = &AllSettings> {
self.settings.values()
}
/// Validate all resolved `Settings` in this `Resolver`.
pub fn validate(&self, strategy: &PyprojectDiscovery) -> Result<()> {
// TODO(charlie): This risks false positives (but not false negatives), since
// some of the `Settings` in the path may ultimately be unused (or, e.g., they
// could have their `required_version` overridden by other `Settings` in
// the path). It'd be preferable to validate once we've determined the
// `Settings` for each path, but that's more expensive.
match &strategy {
PyprojectDiscovery::Fixed(settings) => {
settings.lib.validate()?;
}
PyprojectDiscovery::Hierarchical(default) => {
for settings in std::iter::once(default).chain(self.iter()) {
settings.lib.validate()?;
}
}
}
Ok(())
}
}
pub trait ConfigProcessor: Copy + Send + Sync {
@@ -232,7 +214,6 @@ pub fn is_python_entry(entry: &DirEntry) -> bool {
pub fn python_files_in_path(
paths: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
processor: impl ConfigProcessor,
) -> Result<(Vec<Result<DirEntry, ignore::Error>>, Resolver)> {
// Normalize every path (e.g., convert from relative to absolute).
@@ -256,7 +237,7 @@ pub fn python_files_in_path(
}
// Check if the paths themselves are excluded.
if file_strategy.force_exclude {
if pyproject_strategy.top_level_settings().lib.force_exclude {
paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_strategy));
if paths.is_empty() {
return Ok((vec![], resolver));
@@ -272,7 +253,12 @@ pub fn python_files_in_path(
for path in &paths[1..] {
builder.add(path);
}
builder.standard_filters(file_strategy.respect_gitignore);
builder.standard_filters(
pyproject_strategy
.top_level_settings()
.lib
.respect_gitignore,
);
builder.hidden(false);
let walker = builder.build_parallel();
@@ -369,10 +355,9 @@ pub fn python_files_in_path(
pub fn python_file_at_path(
path: &Path,
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
processor: impl ConfigProcessor,
) -> Result<bool> {
if !file_strategy.force_exclude {
if !pyproject_strategy.top_level_settings().lib.force_exclude {
return Ok(true);
}

View File

@@ -1,12 +1,28 @@
use ruff_macros::derive_message_formats;
use rustpython_ast::Location;
use super::detection::comment_contains_code;
use crate::ast::types::Range;
use crate::define_violation;
use crate::fix::Fix;
use crate::registry::{Diagnostic, Rule};
use crate::settings::{flags, Settings};
use crate::source_code::Locator;
use crate::violations;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
pub struct CommentedOutCode;
);
impl AlwaysAutofixableViolation for CommentedOutCode {
#[derive_message_formats]
fn message(&self) -> String {
format!("Found commented-out code")
}
fn autofix_title(&self) -> String {
"Remove commented-out code".to_string()
}
}
fn is_standalone_comment(line: &str) -> bool {
for char in line.chars() {
@@ -33,7 +49,7 @@ pub fn commented_out_code(
// Verify that the comment is on its own line, and that it contains code.
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
let mut diagnostic = Diagnostic::new(violations::CommentedOutCode, Range::new(start, end));
let mut diagnostic = Diagnostic::new(CommentedOutCode, Range::new(start, end));
if matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::CommentedOutCode)
{

View File

@@ -1,5 +1,7 @@
use rustpython_ast::{Expr, ExprKind, Stmt, StmtKind};
use crate::ast::helpers;
use crate::ast::helpers::{find_keyword, is_const_true};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
@@ -21,7 +23,7 @@ pub fn blind_except(
for exception in ["BaseException", "Exception"] {
if id == exception && checker.is_builtin(exception) {
// If the exception is re-raised, don't flag an error.
if !body.iter().any(|stmt| {
if body.iter().any(|stmt| {
if let StmtKind::Raise { exc, .. } = &stmt.node {
if let Some(exc) = exc {
if let ExprKind::Name { id, .. } = &exc.node {
@@ -36,11 +38,38 @@ pub fn blind_except(
false
}
}) {
checker.diagnostics.push(Diagnostic::new(
violations::BlindExcept(id.to_string()),
Range::from_located(type_),
));
continue;
}
// If the exception is logged, don't flag an error.
if body.iter().any(|stmt| {
if let StmtKind::Expr { value } = &stmt.node {
if let ExprKind::Call { func, keywords, .. } = &value.node {
if helpers::is_logger_candidate(func) {
if let ExprKind::Attribute { attr, .. } = &func.node {
if attr == "exception" {
return true;
}
if attr == "error" {
if let Some(keyword) = find_keyword(keywords, "exc_info") {
if is_const_true(&keyword.node.value) {
return true;
}
}
}
}
}
}
}
false
}) {
continue;
}
checker.diagnostics.push(Diagnostic::new(
violations::BlindExcept(id.to_string()),
Range::from_located(type_),
));
}
}
}

View File

@@ -72,4 +72,34 @@ expression: diagnostics
column: 20
fix: ~
parent: ~
- kind:
BlindExcept: Exception
location:
row: 69
column: 7
end_location:
row: 69
column: 16
fix: ~
parent: ~
- kind:
BlindExcept: Exception
location:
row: 75
column: 7
end_location:
row: 75
column: 16
fix: ~
parent: ~
- kind:
BlindExcept: Exception
location:
row: 81
column: 7
end_location:
row: 81
column: 16
fix: ~
parent: ~

View File

@@ -1,10 +1,42 @@
use ruff_macros::derive_message_formats;
use rustpython_ast::{Arguments, ExprKind};
use rustpython_parser::ast::{Constant, Expr};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::{Diagnostic, DiagnosticKind};
use crate::violations;
use crate::violation::Violation;
define_violation!(
pub struct BooleanPositionalArgInFunctionDefinition;
);
impl Violation for BooleanPositionalArgInFunctionDefinition {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean positional arg in function definition")
}
}
define_violation!(
pub struct BooleanDefaultValueInFunctionDefinition;
);
impl Violation for BooleanDefaultValueInFunctionDefinition {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean default value in function definition")
}
}
define_violation!(
pub struct BooleanPositionalValueInFunctionCall;
);
impl Violation for BooleanPositionalValueInFunctionCall {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean positional value in function call")
}
}
const FUNC_NAME_ALLOWLIST: &[&str] = &[
"assertEqual",
@@ -77,7 +109,7 @@ pub fn check_positional_boolean_in_def(checker: &mut Checker, arguments: &Argume
continue;
}
checker.diagnostics.push(Diagnostic::new(
violations::BooleanPositionalArgInFunctionDefinition,
BooleanPositionalArgInFunctionDefinition,
Range::from_located(arg),
));
}
@@ -88,11 +120,7 @@ pub fn check_boolean_default_value_in_function_definition(
arguments: &Arguments,
) {
for arg in &arguments.defaults {
add_if_boolean(
checker,
arg,
violations::BooleanDefaultValueInFunctionDefinition.into(),
);
add_if_boolean(checker, arg, BooleanDefaultValueInFunctionDefinition.into());
}
}
@@ -105,10 +133,6 @@ pub fn check_boolean_positional_value_in_function_call(
if allow_boolean_trap(func) {
continue;
}
add_if_boolean(
checker,
arg,
violations::BooleanPositionalValueInFunctionCall.into(),
);
add_if_boolean(checker, arg, BooleanPositionalValueInFunctionCall.into());
}
}

View File

@@ -4,7 +4,7 @@ use crate::ast::helpers::unparse_expr;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::python::identifiers::is_identifier;
use crate::python::identifiers::{is_identifier, is_mangled_private};
use crate::python::keyword::KWLIST;
use crate::registry::Diagnostic;
use crate::violations;
@@ -41,12 +41,13 @@ pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
if !is_identifier(value) {
return;
}
if KWLIST.contains(&value.as_str()) {
if KWLIST.contains(&value.as_str()) || is_mangled_private(value.as_str()) {
return;
}
let mut diagnostic =
Diagnostic::new(violations::GetAttrWithConstant, Range::from_located(expr));
if checker.patch(diagnostic.kind.rule()) {
diagnostic.amend(Fix::replacement(
unparse_expr(&attribute(obj, value), checker.stylist),

View File

@@ -4,7 +4,7 @@ use crate::ast::helpers::unparse_stmt;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::python::identifiers::is_identifier;
use crate::python::identifiers::{is_identifier, is_mangled_private};
use crate::python::keyword::KWLIST;
use crate::registry::Diagnostic;
use crate::source_code::Stylist;
@@ -51,7 +51,7 @@ pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
if !is_identifier(name) {
return;
}
if KWLIST.contains(&name.as_str()) {
if KWLIST.contains(&name.as_str()) || is_mangled_private(name.as_str()) {
return;
}
// We can only replace a `setattr` call (which is an `Expr`) with an assignment
@@ -61,6 +61,7 @@ pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
if expr == child.as_ref() {
let mut diagnostic =
Diagnostic::new(violations::SetAttrWithConstant, Range::from_located(expr));
if checker.patch(diagnostic.kind.rule()) {
diagnostic.amend(Fix::replacement(
assignment(obj, name, value, checker.stylist),

View File

@@ -5,109 +5,127 @@ expression: diagnostics
- kind:
GetAttrWithConstant: ~
location:
row: 18
row: 19
column: 0
end_location:
row: 18
row: 19
column: 19
fix:
content:
- foo.bar
location:
row: 18
row: 19
column: 0
end_location:
row: 18
row: 19
column: 19
parent: ~
- kind:
GetAttrWithConstant: ~
location:
row: 19
row: 20
column: 0
end_location:
row: 19
row: 20
column: 23
fix:
content:
- foo._123abc
location:
row: 19
row: 20
column: 0
end_location:
row: 19
row: 20
column: 23
parent: ~
- kind:
GetAttrWithConstant: ~
location:
row: 20
row: 21
column: 0
end_location:
row: 20
row: 21
column: 26
fix:
content:
- foo.__123abc__
location:
row: 21
column: 0
end_location:
row: 21
column: 26
parent: ~
- kind:
GetAttrWithConstant: ~
location:
row: 22
column: 0
end_location:
row: 22
column: 22
fix:
content:
- foo.abc123
location:
row: 20
row: 22
column: 0
end_location:
row: 20
row: 22
column: 22
parent: ~
- kind:
GetAttrWithConstant: ~
location:
row: 21
row: 23
column: 0
end_location:
row: 21
row: 23
column: 23
fix:
content:
- foo.abc123
location:
row: 21
row: 23
column: 0
end_location:
row: 21
row: 23
column: 23
parent: ~
- kind:
GetAttrWithConstant: ~
location:
row: 22
row: 24
column: 14
end_location:
row: 22
row: 24
column: 31
fix:
content:
- x.bar
location:
row: 22
row: 24
column: 14
end_location:
row: 22
row: 24
column: 31
parent: ~
- kind:
GetAttrWithConstant: ~
location:
row: 23
row: 25
column: 3
end_location:
row: 23
row: 25
column: 20
fix:
content:
- x.bar
location:
row: 23
row: 25
column: 3
end_location:
row: 23
row: 25
column: 20
parent: ~

View File

@@ -5,91 +5,109 @@ expression: diagnostics
- kind:
SetAttrWithConstant: ~
location:
row: 37
row: 40
column: 0
end_location:
row: 37
row: 40
column: 25
fix:
content:
- foo.bar = None
location:
row: 37
row: 40
column: 0
end_location:
row: 37
row: 40
column: 25
parent: ~
- kind:
SetAttrWithConstant: ~
location:
row: 38
row: 41
column: 0
end_location:
row: 38
row: 41
column: 29
fix:
content:
- foo._123abc = None
location:
row: 38
row: 41
column: 0
end_location:
row: 38
row: 41
column: 29
parent: ~
- kind:
SetAttrWithConstant: ~
location:
row: 39
row: 42
column: 0
end_location:
row: 39
row: 42
column: 32
fix:
content:
- foo.__123abc__ = None
location:
row: 42
column: 0
end_location:
row: 42
column: 32
parent: ~
- kind:
SetAttrWithConstant: ~
location:
row: 43
column: 0
end_location:
row: 43
column: 28
fix:
content:
- foo.abc123 = None
location:
row: 39
row: 43
column: 0
end_location:
row: 39
row: 43
column: 28
parent: ~
- kind:
SetAttrWithConstant: ~
location:
row: 40
row: 44
column: 0
end_location:
row: 40
row: 44
column: 29
fix:
content:
- foo.abc123 = None
location:
row: 40
row: 44
column: 0
end_location:
row: 40
row: 44
column: 29
parent: ~
- kind:
SetAttrWithConstant: ~
location:
row: 41
row: 45
column: 0
end_location:
row: 41
row: 45
column: 30
fix:
content:
- foo.bar.baz = None
location:
row: 41
row: 45
column: 0
end_location:
row: 41
row: 45
column: 30
parent: ~

View File

@@ -1,12 +1,14 @@
use itertools::Itertools;
use ruff_macros::derive_message_formats;
use rustpython_parser::lexer::{LexResult, Spanned};
use rustpython_parser::token::Tok;
use crate::ast::types::Range;
use crate::define_violation;
use crate::fix::Fix;
use crate::registry::{Diagnostic, Rule};
use crate::settings::{flags, Settings};
use crate::violations;
use crate::violation::{AlwaysAutofixableViolation, Violation};
/// Simplified token type.
#[derive(Copy, Clone, PartialEq, Eq)]
@@ -107,6 +109,44 @@ impl Context {
}
}
define_violation!(
pub struct TrailingCommaMissing;
);
impl AlwaysAutofixableViolation for TrailingCommaMissing {
#[derive_message_formats]
fn message(&self) -> String {
format!("Trailing comma missing")
}
fn autofix_title(&self) -> String {
"Add trailing comma".to_string()
}
}
define_violation!(
pub struct TrailingCommaOnBareTupleProhibited;
);
impl Violation for TrailingCommaOnBareTupleProhibited {
#[derive_message_formats]
fn message(&self) -> String {
format!("Trailing comma on bare tuple prohibited")
}
}
define_violation!(
pub struct TrailingCommaProhibited;
);
impl AlwaysAutofixableViolation for TrailingCommaProhibited {
#[derive_message_formats]
fn message(&self) -> String {
format!("Trailing comma prohibited")
}
fn autofix_title(&self) -> String {
"Remove trailing comma".to_string()
}
}
/// COM812, COM818, COM819
pub fn trailing_commas(
tokens: &[LexResult],
@@ -212,7 +252,7 @@ pub fn trailing_commas(
if comma_prohibited {
let comma = prev.spanned.unwrap();
let mut diagnostic = Diagnostic::new(
violations::TrailingCommaProhibited,
TrailingCommaProhibited,
Range {
location: comma.0,
end_location: comma.2,
@@ -233,7 +273,7 @@ pub fn trailing_commas(
if bare_comma_prohibited {
let comma = prev.spanned.unwrap();
diagnostics.push(Diagnostic::new(
violations::TrailingCommaOnBareTupleProhibited,
TrailingCommaOnBareTupleProhibited,
Range {
location: comma.0,
end_location: comma.2,
@@ -258,7 +298,7 @@ pub fn trailing_commas(
if comma_required {
let missing_comma = prev_prev.spanned.unwrap();
let mut diagnostic = Diagnostic::new(
violations::TrailingCommaMissing,
TrailingCommaMissing,
Range {
location: missing_comma.2,
end_location: missing_comma.2,

View File

@@ -9,7 +9,7 @@ use libcst_native::{
use crate::ast::types::Range;
use crate::cst::matchers::{match_expr, match_module};
use crate::fix::Fix;
use crate::source_code::Locator;
use crate::source_code::{Locator, Stylist};
fn match_call<'a, 'b>(expr: &'a mut Expr<'b>) -> Result<&'a mut Call<'b>> {
if let Expression::Call(call) = &mut expr.value {
@@ -30,6 +30,7 @@ fn match_arg<'a, 'b>(call: &'a Call<'b>) -> Result<&'a Arg<'b>> {
/// (C400) Convert `list(x for x in y)` to `[x for x in y]`.
pub fn fix_unnecessary_generator_list(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
// Expr(Call(GeneratorExp)))) -> Expr(ListComp)))
@@ -58,7 +59,11 @@ pub fn fix_unnecessary_generator_list(
rpar: generator_exp.rpar.clone(),
}));
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -71,6 +76,7 @@ pub fn fix_unnecessary_generator_list(
/// (C401) Convert `set(x for x in y)` to `{x for x in y}`.
pub fn fix_unnecessary_generator_set(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
// Expr(Call(GeneratorExp)))) -> Expr(SetComp)))
@@ -99,7 +105,11 @@ pub fn fix_unnecessary_generator_set(
rpar: generator_exp.rpar.clone(),
}));
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -113,6 +123,7 @@ pub fn fix_unnecessary_generator_set(
/// range(3)}`.
pub fn fix_unnecessary_generator_dict(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
@@ -157,7 +168,11 @@ pub fn fix_unnecessary_generator_dict(
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")),
}));
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -170,6 +185,7 @@ pub fn fix_unnecessary_generator_dict(
/// (C403) Convert `set([x for x in y])` to `{x for x in y}`.
pub fn fix_unnecessary_list_comprehension_set(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
// Expr(Call(ListComp)))) ->
@@ -197,7 +213,11 @@ pub fn fix_unnecessary_list_comprehension_set(
rpar: list_comp.rpar.clone(),
}));
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -211,6 +231,7 @@ pub fn fix_unnecessary_list_comprehension_set(
/// range(3)}`.
pub fn fix_unnecessary_list_comprehension_dict(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
@@ -248,7 +269,11 @@ pub fn fix_unnecessary_list_comprehension_dict(
rpar: list_comp.rpar.clone(),
}));
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -302,7 +327,11 @@ fn drop_trailing_comma<'a>(
}
/// (C405) Convert `set((1, 2))` to `{1, 2}`.
pub fn fix_unnecessary_literal_set(locator: &Locator, expr: &rustpython_ast::Expr) -> Result<Fix> {
pub fn fix_unnecessary_literal_set(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
// Expr(Call(List|Tuple)))) -> Expr(Set)))
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
let mut tree = match_module(module_text)?;
@@ -334,7 +363,11 @@ pub fn fix_unnecessary_literal_set(locator: &Locator, expr: &rustpython_ast::Exp
}));
}
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -345,7 +378,11 @@ pub fn fix_unnecessary_literal_set(locator: &Locator, expr: &rustpython_ast::Exp
}
/// (C406) Convert `dict([(1, 2)])` to `{1: 2}`.
pub fn fix_unnecessary_literal_dict(locator: &Locator, expr: &rustpython_ast::Expr) -> Result<Fix> {
pub fn fix_unnecessary_literal_dict(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
// Expr(Call(List|Tuple)))) -> Expr(Dict)))
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
let mut tree = match_module(module_text)?;
@@ -399,7 +436,11 @@ pub fn fix_unnecessary_literal_dict(locator: &Locator, expr: &rustpython_ast::Ex
rpar: vec![],
}));
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -412,6 +453,7 @@ pub fn fix_unnecessary_literal_dict(locator: &Locator, expr: &rustpython_ast::Ex
/// (C408)
pub fn fix_unnecessary_collection_call(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
// Expr(Call("list" | "tuple" | "dict")))) -> Expr(List|Tuple|Dict)
@@ -508,7 +550,11 @@ pub fn fix_unnecessary_collection_call(
}
};
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -521,6 +567,7 @@ pub fn fix_unnecessary_collection_call(
/// (C409) Convert `tuple([1, 2])` to `tuple(1, 2)`
pub fn fix_unnecessary_literal_within_tuple_call(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
@@ -562,7 +609,11 @@ pub fn fix_unnecessary_literal_within_tuple_call(
}],
}));
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -575,6 +626,7 @@ pub fn fix_unnecessary_literal_within_tuple_call(
/// (C410) Convert `list([1, 2])` to `[1, 2]`
pub fn fix_unnecessary_literal_within_list_call(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
@@ -618,7 +670,11 @@ pub fn fix_unnecessary_literal_within_list_call(
rpar: vec![],
}));
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -629,7 +685,11 @@ pub fn fix_unnecessary_literal_within_list_call(
}
/// (C411) Convert `list([i * i for i in x])` to `[i * i for i in x]`.
pub fn fix_unnecessary_list_call(locator: &Locator, expr: &rustpython_ast::Expr) -> Result<Fix> {
pub fn fix_unnecessary_list_call(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
// Expr(Call(List|Tuple)))) -> Expr(List|Tuple)))
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
let mut tree = match_module(module_text)?;
@@ -639,7 +699,11 @@ pub fn fix_unnecessary_list_call(locator: &Locator, expr: &rustpython_ast::Expr)
body.value = arg.value.clone();
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -654,6 +718,7 @@ pub fn fix_unnecessary_list_call(locator: &Locator, expr: &rustpython_ast::Expr)
/// reverse=True)`.
pub fn fix_unnecessary_call_around_sorted(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
@@ -723,7 +788,11 @@ pub fn fix_unnecessary_call_around_sorted(
}
}
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -736,6 +805,7 @@ pub fn fix_unnecessary_call_around_sorted(
/// (C416) Convert `[i for i in x]` to `list(x)`.
pub fn fix_unnecessary_comprehension(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_ast::Expr,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
@@ -792,7 +862,11 @@ pub fn fix_unnecessary_comprehension(
}
}
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(

View File

@@ -66,7 +66,7 @@ pub fn unnecessary_generator_list(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryGeneratorList) {
match fixes::fix_unnecessary_generator_list(checker.locator, expr) {
match fixes::fix_unnecessary_generator_list(checker.locator, checker.stylist, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -97,7 +97,7 @@ pub fn unnecessary_generator_set(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryGeneratorSet) {
match fixes::fix_unnecessary_generator_set(checker.locator, expr) {
match fixes::fix_unnecessary_generator_set(checker.locator, checker.stylist, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -127,7 +127,11 @@ pub fn unnecessary_generator_dict(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryGeneratorDict) {
match fixes::fix_unnecessary_generator_dict(checker.locator, expr) {
match fixes::fix_unnecessary_generator_dict(
checker.locator,
checker.stylist,
expr,
) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -161,7 +165,11 @@ pub fn unnecessary_list_comprehension_set(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryListComprehensionSet) {
match fixes::fix_unnecessary_list_comprehension_set(checker.locator, expr) {
match fixes::fix_unnecessary_list_comprehension_set(
checker.locator,
checker.stylist,
expr,
) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -200,7 +208,8 @@ pub fn unnecessary_list_comprehension_dict(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryListComprehensionDict) {
match fixes::fix_unnecessary_list_comprehension_dict(checker.locator, expr) {
match fixes::fix_unnecessary_list_comprehension_dict(checker.locator, checker.stylist, expr)
{
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -234,7 +243,7 @@ pub fn unnecessary_literal_set(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryLiteralSet) {
match fixes::fix_unnecessary_literal_set(checker.locator, expr) {
match fixes::fix_unnecessary_literal_set(checker.locator, checker.stylist, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -275,7 +284,7 @@ pub fn unnecessary_literal_dict(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryLiteralDict) {
match fixes::fix_unnecessary_literal_dict(checker.locator, expr) {
match fixes::fix_unnecessary_literal_dict(checker.locator, checker.stylist, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -316,7 +325,7 @@ pub fn unnecessary_collection_call(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryCollectionCall) {
match fixes::fix_unnecessary_collection_call(checker.locator, expr) {
match fixes::fix_unnecessary_collection_call(checker.locator, checker.stylist, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -349,7 +358,11 @@ pub fn unnecessary_literal_within_tuple_call(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryLiteralWithinTupleCall) {
match fixes::fix_unnecessary_literal_within_tuple_call(checker.locator, expr) {
match fixes::fix_unnecessary_literal_within_tuple_call(
checker.locator,
checker.stylist,
expr,
) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -382,7 +395,11 @@ pub fn unnecessary_literal_within_list_call(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryLiteralWithinListCall) {
match fixes::fix_unnecessary_literal_within_list_call(checker.locator, expr) {
match fixes::fix_unnecessary_literal_within_list_call(
checker.locator,
checker.stylist,
expr,
) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -406,7 +423,7 @@ pub fn unnecessary_list_call(checker: &mut Checker, expr: &Expr, func: &Expr, ar
let mut diagnostic =
Diagnostic::new(violations::UnnecessaryListCall, Range::from_located(expr));
if checker.patch(&Rule::UnnecessaryListCall) {
match fixes::fix_unnecessary_list_call(checker.locator, expr) {
match fixes::fix_unnecessary_list_call(checker.locator, checker.stylist, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -449,7 +466,7 @@ pub fn unnecessary_call_around_sorted(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryCallAroundSorted) {
match fixes::fix_unnecessary_call_around_sorted(checker.locator, expr) {
match fixes::fix_unnecessary_call_around_sorted(checker.locator, checker.stylist, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
@@ -612,7 +629,7 @@ pub fn unnecessary_comprehension(
Range::from_located(expr),
);
if checker.patch(&Rule::UnnecessaryComprehension) {
match fixes::fix_unnecessary_comprehension(checker.locator, expr) {
match fixes::fix_unnecessary_comprehension(checker.locator, checker.stylist, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}

View File

@@ -2,7 +2,7 @@ use once_cell::sync::Lazy;
use regex::Regex;
static SHEBANG_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?P<spaces>\s*)#!(?P<directive>.*)").unwrap());
Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)#!(?P<directive>.*)").unwrap());
#[derive(Debug)]
pub enum ShebangDirective<'a> {
@@ -67,7 +67,7 @@ mod tests {
));
assert!(matches!(
extract_shebang("print('test') #!/usr/bin/python"),
ShebangDirective::Match(2, 17, 32, "/usr/bin/python")
ShebangDirective::None
));
}
}

View File

@@ -20,7 +20,7 @@ impl Violation for ShebangPython {
/// EXE003
pub fn shebang_python(lineno: usize, shebang: &ShebangDirective) -> Option<Diagnostic> {
if let ShebangDirective::Match(_, start, end, content) = shebang {
if content.contains("python") {
if content.contains("python") || content.contains("pytest") {
None
} else {
let diagnostic = Diagnostic::new(

View File

@@ -2,5 +2,14 @@
source: src/rules/flake8_executable/mod.rs
expression: diagnostics
---
[]
- kind:
ShebangMissingExecutableFile: ~
location:
row: 1
column: 0
end_location:
row: 1
column: 0
fix: ~
parent: ~

View File

@@ -1,9 +1,22 @@
use ruff_macros::derive_message_formats;
use rustc_hash::FxHashMap;
use rustpython_ast::Stmt;
use crate::ast::types::Range;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violations;
use crate::violation::Violation;
define_violation!(
pub struct ImportAliasIsNotConventional(pub String, pub String);
);
impl Violation for ImportAliasIsNotConventional {
#[derive_message_formats]
fn message(&self) -> String {
let ImportAliasIsNotConventional(name, asname) = self;
format!("`{name}` should be imported as `{asname}`")
}
}
/// ICN001
pub fn check_conventional_import(
@@ -25,10 +38,7 @@ pub fn check_conventional_import(
}
if !is_valid_import {
return Some(Diagnostic::new(
violations::ImportAliasIsNotConventional(
name.to_string(),
expected_alias.to_string(),
),
ImportAliasIsNotConventional(name.to_string(), expected_alias.to_string()),
Range::from_located(import_from),
));
}

View File

@@ -0,0 +1,50 @@
//! Rules from [flake8-logging-format](https://pypi.org/project/flake8-logging-format/0.9.0/).
pub(crate) mod rules;
pub(crate) mod violations;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::Rule;
use crate::settings;
#[test_case(Path::new("G_argparse_parser_error_ok.py"); "G_argparse_parser_error_ok")]
#[test_case(Path::new("G_extra_ok.py"); "G_extra_ok")]
#[test_case(Path::new("G_extra_str_format_ok.py"); "G_extra_str_format_ok")]
#[test_case(Path::new("G_simple_ok.py"); "G_simple_ok")]
#[test_case(Path::new("G_warnings_ok.py"); "G_warnings_ok")]
#[test_case(Path::new("G001.py"); "G001")]
#[test_case(Path::new("G002.py"); "G002")]
#[test_case(Path::new("G003.py"); "G003")]
#[test_case(Path::new("G004.py"); "G004")]
#[test_case(Path::new("G010.py"); "G010")]
#[test_case(Path::new("G101_1.py"); "G101_1")]
#[test_case(Path::new("G101_2.py"); "G101_2")]
#[test_case(Path::new("G201.py"); "G201")]
#[test_case(Path::new("G202.py"); "G202")]
fn rules(path: &Path) -> Result<()> {
let snapshot = path.to_string_lossy().into_owned();
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_logging_format")
.join(path)
.as_path(),
&settings::Settings::for_rules(vec![
Rule::LoggingStringFormat,
Rule::LoggingPercentFormat,
Rule::LoggingStringConcat,
Rule::LoggingFString,
Rule::LoggingWarn,
Rule::LoggingExtraAttrClash,
Rule::LoggingExcInfo,
Rule::LoggingRedundantExcInfo,
]),
)?;
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -0,0 +1,235 @@
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Operator};
use rustpython_parser::ast::Location;
use crate::ast::helpers::{find_keyword, is_logger_candidate, SimpleCallArgs};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::{Diagnostic, Rule};
use crate::rules::flake8_logging_format::violations::{
LoggingExcInfo, LoggingExtraAttrClash, LoggingFString, LoggingPercentFormat,
LoggingRedundantExcInfo, LoggingStringConcat, LoggingStringFormat, LoggingWarn,
};
enum LoggingLevel {
Debug,
Critical,
Error,
Exception,
Info,
Warn,
Warning,
}
impl LoggingLevel {
fn from_str(level: &str) -> Option<Self> {
match level {
"debug" => Some(LoggingLevel::Debug),
"critical" => Some(LoggingLevel::Critical),
"error" => Some(LoggingLevel::Error),
"exception" => Some(LoggingLevel::Exception),
"info" => Some(LoggingLevel::Info),
"warn" => Some(LoggingLevel::Warn),
"warning" => Some(LoggingLevel::Warning),
_ => None,
}
}
}
const RESERVED_ATTRS: &[&str; 22] = &[
"args",
"asctime",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"levelname",
"levelno",
"lineno",
"module",
"msecs",
"message",
"msg",
"name",
"pathname",
"process",
"processName",
"relativeCreated",
"stack_info",
"thread",
"threadName",
];
/// Check logging messages for violations.
fn check_msg(checker: &mut Checker, msg: &Expr) {
match &msg.node {
// Check for string concatenation and percent format.
ExprKind::BinOp { op, .. } => match op {
Operator::Add => {
if checker.settings.rules.enabled(&Rule::LoggingStringConcat) {
checker.diagnostics.push(Diagnostic::new(
LoggingStringConcat,
Range::from_located(msg),
));
}
}
Operator::Mod => {
if checker.settings.rules.enabled(&Rule::LoggingPercentFormat) {
checker.diagnostics.push(Diagnostic::new(
LoggingPercentFormat,
Range::from_located(msg),
));
}
}
_ => {}
},
// Check for f-strings.
ExprKind::JoinedStr { .. } => {
if checker.settings.rules.enabled(&Rule::LoggingFString) {
checker
.diagnostics
.push(Diagnostic::new(LoggingFString, Range::from_located(msg)));
}
}
// Check for .format() calls.
ExprKind::Call { func, .. } => {
if checker.settings.rules.enabled(&Rule::LoggingStringFormat) {
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if attr == "format" && matches!(value.node, ExprKind::Constant { .. }) {
checker.diagnostics.push(Diagnostic::new(
LoggingStringFormat,
Range::from_located(msg),
));
}
}
}
}
_ => {}
}
}
/// Check contents of the `extra` argument to logging calls.
fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) {
match &extra.node.value.node {
ExprKind::Dict { keys, .. } => {
for key in keys {
if let Some(key) = &key {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &key.node
{
if RESERVED_ATTRS.contains(&string.as_str()) {
checker.diagnostics.push(Diagnostic::new(
LoggingExtraAttrClash(string.to_string()),
Range::from_located(key),
));
}
}
}
}
}
ExprKind::Call { func, keywords, .. } => {
if checker
.resolve_call_path(func)
.map_or(false, |call_path| call_path.as_slice() == ["", "dict"])
{
for keyword in keywords {
if let Some(key) = &keyword.node.arg {
if RESERVED_ATTRS.contains(&key.as_str()) {
checker.diagnostics.push(Diagnostic::new(
LoggingExtraAttrClash(key.to_string()),
Range::from_located(keyword),
));
}
}
}
}
}
_ => {}
}
}
/// Check logging calls for violations.
pub fn logging_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: &[Keyword]) {
if !is_logger_candidate(func) {
return;
}
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if let Some(logging_level) = LoggingLevel::from_str(attr.as_str()) {
let call_args = SimpleCallArgs::new(args, keywords);
let level_call_range = Range::new(
Location::new(
func.location.row(),
value.end_location.unwrap().column() + 1,
),
Location::new(
func.end_location.unwrap().row(),
func.end_location.unwrap().column(),
),
);
// G001 - G004
if let Some(format_arg) = call_args.get_argument("msg", Some(0)) {
check_msg(checker, format_arg);
}
// G010
if checker.settings.rules.enabled(&Rule::LoggingWarn)
&& matches!(logging_level, LoggingLevel::Warn)
{
let mut diagnostic = Diagnostic::new(LoggingWarn, level_call_range);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.amend(Fix::replacement(
"warning".to_string(),
level_call_range.location,
level_call_range.end_location,
));
}
checker.diagnostics.push(diagnostic);
}
// G101
if checker.settings.rules.enabled(&Rule::LoggingExtraAttrClash) {
if let Some(extra) = find_keyword(keywords, "extra") {
check_log_record_attr_clash(checker, extra);
}
}
// G201, G202
if checker.settings.rules.enabled(&Rule::LoggingExcInfo)
|| checker
.settings
.rules
.enabled(&Rule::LoggingRedundantExcInfo)
{
if let Some(exc_info) = find_keyword(keywords, "exc_info") {
match logging_level {
LoggingLevel::Error => {
if checker.settings.rules.enabled(&Rule::LoggingExcInfo) {
checker
.diagnostics
.push(Diagnostic::new(LoggingExcInfo, level_call_range));
}
}
LoggingLevel::Exception => {
if checker
.settings
.rules
.enabled(&Rule::LoggingRedundantExcInfo)
{
checker.diagnostics.push(Diagnostic::new(
LoggingRedundantExcInfo,
Range::from_located(exc_info),
));
}
}
_ => {}
}
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
- kind:
LoggingStringFormat: ~
location:
row: 3
column: 13
end_location:
row: 3
column: 40
fix: ~
parent: ~

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
- kind:
LoggingPercentFormat: ~
location:
row: 3
column: 13
end_location:
row: 3
column: 34
fix: ~
parent: ~

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
- kind:
LoggingStringConcat: ~
location:
row: 3
column: 13
end_location:
row: 3
column: 37
fix: ~
parent: ~

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
- kind:
LoggingFString: ~
location:
row: 4
column: 13
end_location:
row: 4
column: 28
fix: ~
parent: ~

View File

@@ -0,0 +1,22 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
- kind:
LoggingWarn: ~
location:
row: 3
column: 8
end_location:
row: 3
column: 12
fix:
content: warning
location:
row: 3
column: 8
end_location:
row: 3
column: 12
parent: ~

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
- kind:
LoggingExtraAttrClash: name
location:
row: 6
column: 8
end_location:
row: 6
column: 14
fix: ~
parent: ~

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
- kind:
LoggingExtraAttrClash: name
location:
row: 6
column: 8
end_location:
row: 6
column: 21
fix: ~
parent: ~

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
- kind:
LoggingExcInfo: ~
location:
row: 3
column: 8
end_location:
row: 3
column: 13
fix: ~
parent: ~

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
- kind:
LoggingRedundantExcInfo: ~
location:
row: 3
column: 33
end_location:
row: 3
column: 46
fix: ~
parent: ~

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_logging_format/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,91 @@
use ruff_macros::derive_message_formats;
use crate::define_violation;
use crate::violation::{AlwaysAutofixableViolation, Violation};
define_violation!(
pub struct LoggingStringFormat;
);
impl Violation for LoggingStringFormat {
#[derive_message_formats]
fn message(&self) -> String {
format!("Logging statement uses `string.format()`")
}
}
define_violation!(
pub struct LoggingPercentFormat;
);
impl Violation for LoggingPercentFormat {
#[derive_message_formats]
fn message(&self) -> String {
format!("Logging statement uses `%`")
}
}
define_violation!(
pub struct LoggingStringConcat;
);
impl Violation for LoggingStringConcat {
#[derive_message_formats]
fn message(&self) -> String {
format!("Logging statement uses `+`")
}
}
define_violation!(
pub struct LoggingFString;
);
impl Violation for LoggingFString {
#[derive_message_formats]
fn message(&self) -> String {
format!("Logging statement uses f-string")
}
}
define_violation!(
pub struct LoggingWarn;
);
impl AlwaysAutofixableViolation for LoggingWarn {
#[derive_message_formats]
fn message(&self) -> String {
format!("Logging statement uses `warn` instead of `warning`")
}
fn autofix_title(&self) -> String {
"Convert to `warn`".to_string()
}
}
define_violation!(
pub struct LoggingExtraAttrClash(pub String);
);
impl Violation for LoggingExtraAttrClash {
#[derive_message_formats]
fn message(&self) -> String {
let LoggingExtraAttrClash(key) = self;
format!(
"Logging statement uses an extra field that clashes with a LogRecord field: `{key}`"
)
}
}
define_violation!(
pub struct LoggingExcInfo;
);
impl Violation for LoggingExcInfo {
#[derive_message_formats]
fn message(&self) -> String {
format!("Logging `.exception(...)` should be used instead of `.error(..., exc_info=True)`")
}
}
define_violation!(
pub struct LoggingRedundantExcInfo;
);
impl Violation for LoggingRedundantExcInfo {
#[derive_message_formats]
fn message(&self) -> String {
format!("Logging statement has redundant `exc_info`")
}
}

View File

@@ -1,14 +1,28 @@
use std::path::Path;
use ruff_macros::derive_message_formats;
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::{fs, violations};
use crate::violation::Violation;
use crate::{define_violation, fs};
define_violation!(
pub struct ImplicitNamespacePackage(pub String);
);
impl Violation for ImplicitNamespacePackage {
#[derive_message_formats]
fn message(&self) -> String {
let ImplicitNamespacePackage(filename) = self;
format!("File `{filename}` is part of an implicit namespace package. Add an `__init__.py`.")
}
}
/// INP001
pub fn implicit_namespace_package(path: &Path, package: Option<&Path>) -> Option<Diagnostic> {
if package.is_none() {
Some(Diagnostic::new(
violations::ImplicitNamespacePackage(fs::relativize_path(path)),
ImplicitNamespacePackage(fs::relativize_path(path)),
Range::default(),
))
} else {

View File

@@ -1,16 +1,96 @@
use log::error;
use ruff_macros::derive_message_formats;
use rustc_hash::FxHashSet;
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::comparable::ComparableExpr;
use crate::ast::helpers::unparse_expr;
use crate::ast::helpers::{match_trailing_comment, unparse_expr};
use crate::ast::types::{Range, RefEquality};
use crate::autofix::helpers::delete_stmt;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::fix::Fix;
use crate::message::Location;
use crate::python::identifiers::is_identifier;
use crate::registry::{Diagnostic, Rule};
use crate::violations;
use crate::violation::{AlwaysAutofixableViolation, Violation};
define_violation!(
pub struct NoUnnecessaryPass;
);
impl AlwaysAutofixableViolation for NoUnnecessaryPass {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary `pass` statement")
}
fn autofix_title(&self) -> String {
"Remove unnecessary `pass`".to_string()
}
}
define_violation!(
pub struct DupeClassFieldDefinitions(pub String);
);
impl AlwaysAutofixableViolation for DupeClassFieldDefinitions {
#[derive_message_formats]
fn message(&self) -> String {
let DupeClassFieldDefinitions(name) = self;
format!("Class field `{name}` is defined multiple times")
}
fn autofix_title(&self) -> String {
let DupeClassFieldDefinitions(name) = self;
format!("Remove duplicate field definition for `{name}`")
}
}
define_violation!(
pub struct PreferUniqueEnums {
pub value: String,
}
);
impl Violation for PreferUniqueEnums {
#[derive_message_formats]
fn message(&self) -> String {
let PreferUniqueEnums { value } = self;
format!("Enum contains duplicate value: `{value}`")
}
}
define_violation!(
pub struct NoUnnecessarySpread;
);
impl Violation for NoUnnecessarySpread {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary spread `**`")
}
}
define_violation!(
pub struct NoUnnecessaryDictKwargs;
);
impl Violation for NoUnnecessaryDictKwargs {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary `dict` kwargs")
}
}
define_violation!(
pub struct PreferListBuiltin;
);
impl AlwaysAutofixableViolation for PreferListBuiltin {
#[derive_message_formats]
fn message(&self) -> String {
format!("Prefer `list` over useless lambda")
}
fn autofix_title(&self) -> String {
"Replace with `list`".to_string()
}
}
/// PIE790
pub fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) {
@@ -30,17 +110,32 @@ pub fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) {
}
) {
if matches!(pass_stmt.node, StmtKind::Pass) {
let mut diagnostic = Diagnostic::new(
violations::NoUnnecessaryPass,
Range::from_located(pass_stmt),
);
let mut diagnostic =
Diagnostic::new(NoUnnecessaryPass, Range::from_located(pass_stmt));
if checker.patch(&Rule::NoUnnecessaryPass) {
match delete_stmt(pass_stmt, None, &[], checker.locator, checker.indexer) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => {
error!("Failed to delete `pass` statement: {}", e);
if let Some(index) = match_trailing_comment(pass_stmt, checker.locator) {
diagnostic.amend(Fix::deletion(
pass_stmt.location,
Location::new(
pass_stmt.end_location.unwrap().row(),
pass_stmt.end_location.unwrap().column() + index,
),
));
} else {
match delete_stmt(
pass_stmt,
None,
&[],
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => {
error!("Failed to delete `pass` statement: {}", e);
}
}
}
}
@@ -84,7 +179,7 @@ pub fn dupe_class_field_definitions<'a, 'b>(
if !seen_targets.insert(target) {
let mut diagnostic = Diagnostic::new(
violations::DupeClassFieldDefinitions(target.to_string()),
DupeClassFieldDefinitions(target.to_string()),
Range::from_located(stmt),
);
if checker.patch(&Rule::DupeClassFieldDefinitions) {
@@ -94,7 +189,14 @@ pub fn dupe_class_field_definitions<'a, 'b>(
.map(std::convert::Into::into)
.collect();
let locator = checker.locator;
match delete_stmt(stmt, Some(parent), &deleted, locator, checker.indexer) {
match delete_stmt(
stmt,
Some(parent),
&deleted,
locator,
checker.indexer,
checker.stylist,
) {
Ok(fix) => {
checker.deletions.insert(RefEquality(stmt));
diagnostic.amend(fix);
@@ -143,7 +245,7 @@ where
if !seen_targets.insert(ComparableExpr::from(value)) {
let diagnostic = Diagnostic::new(
violations::PreferUniqueEnums {
PreferUniqueEnums {
value: unparse_expr(value, checker.stylist),
},
Range::from_located(stmt),
@@ -160,8 +262,7 @@ pub fn no_unnecessary_spread(checker: &mut Checker, keys: &[Option<Expr>], value
// We only care about when the key is None which indicates a spread `**`
// inside a dict.
if let ExprKind::Dict { .. } = value.node {
let diagnostic =
Diagnostic::new(violations::NoUnnecessarySpread, Range::from_located(value));
let diagnostic = Diagnostic::new(NoUnnecessarySpread, Range::from_located(value));
checker.diagnostics.push(diagnostic);
}
}
@@ -192,10 +293,8 @@ pub fn no_unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[
// handle case of foo(**{**bar})
(keys.len() == 1 && keys[0].is_none())
{
let diagnostic = Diagnostic::new(
violations::NoUnnecessaryDictKwargs,
Range::from_located(expr),
);
let diagnostic =
Diagnostic::new(NoUnnecessaryDictKwargs, Range::from_located(expr));
checker.diagnostics.push(diagnostic);
}
}
@@ -211,8 +310,7 @@ pub fn prefer_list_builtin(checker: &mut Checker, expr: &Expr) {
if args.args.is_empty() {
if let ExprKind::List { elts, .. } = &body.node {
if elts.is_empty() {
let mut diagnostic =
Diagnostic::new(violations::PreferListBuiltin, Range::from_located(expr));
let mut diagnostic = Diagnostic::new(PreferListBuiltin, Range::from_located(expr));
if checker.patch(&Rule::PreferListBuiltin) {
diagnostic.amend(Fix::replacement(
"list".to_string(),

View File

@@ -290,4 +290,22 @@ expression: diagnostics
row: 97
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
location:
row: 101
column: 4
end_location:
row: 101
column: 8
fix:
content:
- ""
location:
row: 101
column: 4
end_location:
row: 101
column: 10
parent: ~

View File

@@ -63,6 +63,7 @@ pub fn print_call(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) {
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {

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