Compare commits

...

121 Commits

Author SHA1 Message Date
Charlie Marsh
51bda28a7d Bump version to 0.0.192 2022-12-22 17:31:31 -05:00
Reiner Gerecke
cc26051b7a Implement "datetime.UTC alias" check from pyupgrade (#1341) 2022-12-22 17:21:36 -05:00
Charlie Marsh
3ac5a9aa31 Respect --force-exclude for files passed via stdin (#1342) 2022-12-22 16:40:15 -05:00
Charlie Marsh
451047c30d Exclude directly-passed files nested in excluded subdirectories 2022-12-22 15:08:11 -05:00
Charlie Marsh
6907df489b Extend false-positive list for flake8-boolean-trap (#1338) 2022-12-22 10:56:04 -05:00
Charlie Marsh
970f882b03 Set force-exclude for pre-commit in README (#1337) 2022-12-22 10:51:20 -05:00
Charlie Marsh
3eff9a2860 Allow unittest methods in flake8-boolean-trap (#1333) 2022-12-22 08:40:22 -05:00
Charlie Marsh
a4a24a0ef3 Add some more repositories to the user list (#1328) 2022-12-21 22:16:47 -05:00
Charlie Marsh
48e3c046b0 Fix integration tests 2022-12-21 21:25:37 -05:00
Charlie Marsh
03e4f5be8a Bump version to 0.0.191 2022-12-21 21:16:21 -05:00
Charlie Marsh
99657b7d92 Implement E401 ("multiple imports on one line") (#1326) 2022-12-21 21:15:57 -05:00
Charlie Marsh
40377aa1fc Move number of errors to the bottom of the output summary (#1325) 2022-12-21 21:04:26 -05:00
Charlie Marsh
2a37017e8c Add src to Settings hash 2022-12-21 21:01:20 -05:00
Charlie Marsh
ff66d08cef Run generate-options 2022-12-21 20:58:14 -05:00
Charlie Marsh
dad8035eef Support shell expansion in src field (#1324) 2022-12-21 20:57:20 -05:00
Charlie Marsh
bf5fec342c Support shell expansion in extend paths (#1323) 2022-12-21 20:46:38 -05:00
Charlie Marsh
66a6c81ebf Infer package roots when running via stdin (#1321) 2022-12-21 20:30:10 -05:00
Charlie Marsh
5c70f5044b Improve debug logging in flake8-to-ruff (#1320) 2022-12-21 20:05:48 -05:00
Charlie Marsh
953d141ab2 Support code redirects in flake8-to-ruff (#1318) 2022-12-21 19:31:20 -05:00
Charlie Marsh
07dba46039 Extract line length from pyproject.toml Black section (#1317) 2022-12-21 19:05:18 -05:00
Ran Benita
3b02da9d7b Fix false positive DTZ001 on datetime(2000, 1, 1, 0, 0, 0, 0, utc) (#1308) 2022-12-21 19:03:36 -05:00
Charlie Marsh
20234c6156 Bump version to 0.0.190 2022-12-21 16:01:48 -05:00
Charlie Marsh
de767cc026 Avoid used-prior-global-declaration false-positives in f-strings (#1314) 2022-12-21 14:34:09 -05:00
Charlie Marsh
ce1663d302 Allow overriding cache location via RUFF_CACHE_DIR (#1312) 2022-12-21 14:24:10 -05:00
Charlie Marsh
f40e4bcd14 Avoid flagging RUF100 as a RUF100 violation (#1305) 2022-12-20 17:40:10 -05:00
Charlie Marsh
e7d40d435f Avoid F821 false positives for Mypy extensions (#1304) 2022-12-20 16:29:33 -05:00
Charlie Marsh
ef8fe31c0c Bump version to 0.0.189 2022-12-20 13:26:17 -05:00
Charlie Marsh
226f682c99 Avoid DTZ007 false-positives for non-string arguments (#1300) 2022-12-20 13:20:53 -05:00
Hannes Käufler
468ffd29fb [Stylistic/non-functional] Use an r# format string to make json easier to read (#1299) 2022-12-20 12:55:21 -05:00
Charlie Marsh
a61126ab23 Run generate-options 2022-12-19 23:19:05 -05:00
Charlie Marsh
54c7c25861 Revert test changes to setup.py 2022-12-19 20:09:47 -05:00
Charlie Marsh
eff7700d92 Add --force-exclude setting to force exclusions with pre-commit (#1295) 2022-12-19 20:08:59 -05:00
Charlie Marsh
8934f6938d Avoid RET504 errors for intermediary function calls (#1294) 2022-12-19 19:48:09 -05:00
Charlie Marsh
8f0fc3033a Update Arg section checking to match latest pydocstyle (#1293) 2022-12-19 16:39:42 -05:00
Charlie Marsh
4107bc828d Bump version to 0.0.188 2022-12-19 12:18:06 -05:00
Charlie Marsh
706d28cabc Rename PDV checks to PD (#1288) 2022-12-19 00:20:28 -05:00
Charlie Marsh
4da2264722 Avoid T201 errors for print(..., file=fp)-like calls (#1287) 2022-12-19 00:10:07 -05:00
Charlie Marsh
bf88c815aa Move flake8-debugger tests into flake8-debugger subdirectory (#1286) 2022-12-18 22:06:42 -05:00
Yasu_umi
8a4831dd5b Implement flake8-datetimez (#1270) 2022-12-18 22:06:06 -05:00
Charlie Marsh
b5ab492a70 Bump version to 0.0.187 2022-12-18 20:09:02 -05:00
Charlie Marsh
1fc09ebd5c Fix inverted E501 condition (#1285) 2022-12-18 20:08:30 -05:00
Charlie Marsh
6cf047976c Bump pygrep-hooks tally in README 2022-12-18 18:05:32 -05:00
Reiner Gerecke
87465daacc pygrep-hooks - deprecated use of logging.warn & no blanket type ignore (#1275) 2022-12-18 18:04:21 -05:00
Chris Brendel
a52bed7101 Use --stdin-filename when resolving configuration files (#1281) 2022-12-18 17:51:55 -05:00
Anders Kaseorg
20ac823778 generate-check-code-prefix: Run rustfmt automatically; only write if changed (#1282) 2022-12-18 17:46:23 -05:00
Charlie Marsh
1028ed3565 Bump version to 0.0.186 2022-12-18 14:30:30 -05:00
Anders Kaseorg
98897db6ac Add packaging status badge from repology (#1276) 2022-12-18 14:27:29 -05:00
Honkertonken
5ce4262112 Readme : Fix incorrect exmaple. (#1277) 2022-12-18 12:04:48 -05:00
Charlie Marsh
6b2359384d Print redirect warnings exactly once per code (#1280) 2022-12-18 12:03:49 -05:00
Harutaka Kawamura
d3443d7c19 Update RustPython to use correct Tuple location (#1278) 2022-12-18 08:53:57 -05:00
Anders Kaseorg
04b1e1de6f README: Add missing backtick (#1274) 2022-12-18 00:29:33 -05:00
Anders Kaseorg
c93c85300f Repair corrupted PDV007, PDV009 messages (#1273) 2022-12-18 00:29:12 -05:00
Charlie Marsh
73ed6f8654 Touch-up README 2022-12-17 21:38:45 -05:00
Charlie Marsh
eb183645f3 Add ruff-lsp to README (#1272)
Add ruff-lsp to README
2022-12-17 21:37:11 -05:00
Charlie Marsh
ef17aa93da Add ruff-lsp to README (#1272) 2022-12-17 21:37:00 -05:00
Charlie Marsh
f366b0147f Add ruff-lsp to README (#1272) 2022-12-17 21:35:44 -05:00
Charlie Marsh
fc88fa35ff Add instructions for Sublime Text installation (#1271) 2022-12-17 16:22:50 -05:00
Charlie Marsh
a2806eb8ef Bump version to 0.0.185 2022-12-16 23:47:56 -05:00
Charlie Marsh
89d919eac5 Re-remove W605_1.py from Black compatibility test 2022-12-16 23:17:27 -05:00
Charlie Marsh
5ad77fbc8d Move checkers into their own module (#1268) 2022-12-16 22:55:47 -05:00
Charlie Marsh
9cb18a481b Separate line-based checker from noqa enforcement (#1267) 2022-12-16 22:49:27 -05:00
Charlie Marsh
2393e270ed Change a few more methods to take AsRef<Path> 2022-12-16 21:38:52 -05:00
Charlie Marsh
f36e6035c8 Change a few methods to take AsRef<Path> 2022-12-16 21:28:19 -05:00
Charlie Marsh
ecf0dd05d6 Auto-detect same-package imports (#1266) 2022-12-16 21:19:11 -05:00
Charlie Marsh
5f67ee93f7 Replace cache bool with an enum 2022-12-16 15:45:30 -05:00
Charlie Marsh
1e19142d0e Bump version to 0.0.184 2022-12-16 14:36:25 -05:00
Charlie Marsh
6a95dade6d Actually check-in snapshots for #1265 2022-12-16 14:36:00 -05:00
Charlie Marsh
d6e765877e Enable autofix for __init__ method with missing None-return (#1265) 2022-12-16 14:28:56 -05:00
Charlie Marsh
e4d36bae57 Replace ignore_noqa and autofix booleans with enums (#1264) 2022-12-16 14:01:25 -05:00
Harutaka Kawamura
e3531276a7 Fix F501 (line-too-long) start location (#1262) 2022-12-16 11:29:47 -05:00
Charlie Marsh
634553f188 Add ignore-variadic-names options to flake8-unused-arguments (#1261) 2022-12-16 00:22:38 -05:00
Edgar R. M
4ff0b75045 test: Fix flake8-errmsg snapshots (#1260) 2022-12-15 23:53:15 -05:00
Charlie Marsh
481d668511 Add flake8-errmsg to README 2022-12-15 23:16:38 -05:00
Charlie Marsh
a9f56ee76e Bump version to 0.0.183 2022-12-15 23:15:12 -05:00
Charlie Marsh
b4bfa87104 Avoid removing partially-unused imports (#1259) 2022-12-15 23:13:58 -05:00
Charlie Marsh
b9f42bf5e5 Remove extraneous test file 2022-12-15 23:12:19 -05:00
Edgar R. M
8281d414ca Implement flake8-errmsg (#1258) 2022-12-15 23:10:59 -05:00
Charlie Marsh
7e45a9f2e2 Avoid generating invalid statements when deleting from multi-statement lines (#1253) 2022-12-15 22:17:31 -05:00
Reiner Gerecke
a000cd4a09 Test to prevent continious reformatting when used together with black (#1206) 2022-12-15 15:26:41 -05:00
Martin Lehoux
d8b4b92733 Implement U016: Remove six compatibility code (#1013) 2022-12-15 14:16:58 -05:00
Edgar R. M
27de342e75 Implement pandas-vet (#1235) 2022-12-15 14:01:01 -05:00
Charlie Marsh
d805067683 Avoid fixing E711 and E712 issues that would cause F632 (#1248) 2022-12-15 12:08:31 -05:00
Charlie Marsh
1ea2e93f8e Bump version to 0.0.182 2022-12-14 22:57:22 -05:00
Charlie Marsh
dc180dc277 Negate ignore_names condition 2022-12-14 22:50:26 -05:00
Charlie Marsh
6be910ae07 Use more precise ranges for function and class checks (#1247) 2022-12-14 22:40:00 -05:00
Charlie Marsh
ba85eb846c Run cargo fmt 2022-12-14 21:52:44 -05:00
Charlie Marsh
d067efe265 Treat extend-* configuration options as "always extended" (#1245) 2022-12-14 20:22:40 -05:00
Charlie Marsh
549ea2f85f Ignore any pyproject.toml without a [tool.ruff] section (#1243) 2022-12-14 19:35:52 -05:00
Charlie Marsh
d814ebd21f Bump version to 0.0.181 2022-12-14 17:35:36 -05:00
Charlie Marsh
3f272b6cf8 Enable opt-out of .gitignore checks via respect-gitignore flag (#1242) 2022-12-14 16:54:23 -05:00
Charlie Marsh
76891a8c07 Always check zero-depth CLI paths (#1241) 2022-12-14 16:32:02 -05:00
Charlie Marsh
e389201b5f Add new .gitignore behavior to BREAKING_CHANGES.md (#1240) 2022-12-14 16:04:06 -05:00
Charlie Marsh
4b2020d03a Automatically ignore files specified in .gitignore (#1234) 2022-12-14 15:58:40 -05:00
Charlie Marsh
0aa356c96c Avoid converting expression to statement in invald contexts (#1239) 2022-12-14 13:57:25 -05:00
Charlie Marsh
630b4b627d Apply fix to all errors in E711 and E712 autofix (#1238) 2022-12-14 13:29:56 -05:00
Charlie Marsh
854cd14842 Bump version to 0.0.180 2022-12-14 13:21:10 -05:00
Chris Brendel
6b93c8403f Apply CLI options even when no pyproject.toml is found (#1232) 2022-12-13 22:55:04 -05:00
Charlie Marsh
765d21c7b0 Bump version to 0.0.179 2022-12-13 10:17:16 -05:00
Charlie Marsh
a58b9b5063 Upgrade RustPython to support parenthesized context managers (#1228) 2022-12-13 10:16:43 -05:00
Charlie Marsh
f3e11a30cb Bump version to 0.0.178 2022-12-12 22:06:04 -05:00
Charlie Marsh
2f3b5367ff Add a note on extends to README 2022-12-12 21:36:39 -05:00
Charlie Marsh
92bc417e4e Add support for glob patterns in src (#1225) 2022-12-12 21:35:03 -05:00
Charlie Marsh
9853b0728b Enable configuration files to "extend" other configuration files (#1219) 2022-12-12 20:28:22 -05:00
Charlie Marsh
77709dcc41 Remove underscore from extend_exclude 2022-12-12 16:34:16 -05:00
Charlie Marsh
b0cb5fc7ef Document current behavior around pyproject.toml discovery (#1213) 2022-12-12 11:49:21 -05:00
Charlie Marsh
d6f51e55dd Remove extraneous test_project 2022-12-12 10:53:12 -05:00
Charlie Marsh
4bb6b4851a Rename p to path 2022-12-12 10:51:24 -05:00
Charlie Marsh
54c5ded938 Move settings path discovery into its own function 2022-12-12 10:50:08 -05:00
Charlie Marsh
0157fedab5 Move Python file resolution into resolver.rs (#1211) 2022-12-12 10:43:50 -05:00
Charlie Marsh
cd69610741 Use --config everywhere if provided (#1210) 2022-12-12 10:28:00 -05:00
Charlie Marsh
a3d06d0005 Move more commands into commands.rs (#1209) 2022-12-12 10:22:47 -05:00
Charlie Marsh
ac6fa1dc88 Simplify some logic around configuration detection (#1197) 2022-12-12 10:15:05 -05:00
Charlie Marsh
73794fc299 Resolve hierarchical settings and Python files in a single filesystem pass (#1205) 2022-12-12 10:13:52 -05:00
Charlie Marsh
0adc9ed259 Support hierarchical settings for nested directories (#1190) 2022-12-12 10:12:23 -05:00
Charlie Marsh
19e9eb1af8 Bump version to 0.0.177 2022-12-11 22:38:52 -05:00
Anders Kaseorg
e57044800c Fix quotes in SIM118 error message (#1204) 2022-12-11 22:30:39 -05:00
Charlie Marsh
ae8ff7cb7f Add notes around python-lsp-ruff (#1202) 2022-12-11 17:36:20 -05:00
Charlie Marsh
c05914f222 Avoid inserting extra newlines for comment-delimited import blocks (#1201) 2022-12-11 17:13:09 -05:00
Charlie Marsh
24179655b8 Fix 'a test' reference 2022-12-11 13:31:14 -05:00
Charlie Marsh
d27b419e68 Run cargo dev generate-options 2022-12-11 10:34:52 -05:00
Charlie Marsh
9fc7a32a24 Sort list in README 2022-12-11 10:25:27 -05:00
325 changed files with 10770 additions and 2690 deletions

View File

@@ -39,7 +39,7 @@ jobs:
- run: ./target/release/ruff_dev generate-rules-table
- run: ./target/release/ruff_dev generate-options
- run: git diff --quiet README.md || echo "::error file=README.md::This file is outdated. You may have to rerun 'cargo dev generate-options' and/or 'cargo dev generate-rules-table'."
- run: ./target/release/ruff_dev generate-check-code-prefix && cargo fmt -- src/checks_gen.rs
- run: ./target/release/ruff_dev generate-check-code-prefix
- run: git diff --quiet src/checks_gen.rs || echo "::error file=src/checks_gen.rs::This file is outdated. You may have to rerun 'cargo dev generate-check-code-prefix'."
- run: git diff --exit-code -- README.md src/checks_gen.rs
@@ -115,7 +115,9 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- run: pip install black[d]==22.12.0
- run: cargo test --all
- run: cargo test --package ruff --test black_compatibility_test -- --ignored
maturin_build:
name: "maturin build"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.176
rev: v0.0.192
hooks:
- id: ruff

23
BREAKING_CHANGES.md Normal file
View File

@@ -0,0 +1,23 @@
# Breaking Changes
## 0.0.181
### Files excluded by `.gitignore` are now ignored ([#1234](https://github.com/charliermarsh/ruff/pull/1234))
Ruff will now avoid checking files that are excluded by `.ignore`, `.gitignore`,
`.git/info/exclude`, and global `gitignore` files. This behavior is powered by the [`ignore`](https://docs.rs/ignore/latest/ignore/struct.WalkBuilder.html#ignore-rules)
crate, and is applied in addition to Ruff's built-in `exclude` system.
To disable this behavior, set `respect-gitignore = false` in your `pyproject.toml` file.
Note that hidden files (i.e., files and directories prefixed with a `.`) are _not_ ignored by
default.
## 0.0.178
### Configuration files are now resolved hierarchically ([#1190](https://github.com/charliermarsh/ruff/pull/1190))
`pyproject.toml` files are now resolved hierarchically, such that for each Python file, we find
the first `pyproject.toml` file in its path, and use that to determine its lint settings.
See the [README](https://github.com/charliermarsh/ruff#pyprojecttoml-discovery) for more.

64
Cargo.lock generated
View File

@@ -724,7 +724,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.176-dev.0"
version = "0.0.192-dev.0"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -735,6 +735,8 @@ dependencies = [
"rustc-hash",
"serde",
"serde_json",
"strum",
"strum_macros",
"toml",
]
@@ -796,6 +798,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "globset"
version = "0.4.9"
@@ -888,6 +896,24 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "ignore"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
dependencies = [
"crossbeam-utils",
"globset",
"lazy_static",
"log",
"memchr",
"regex",
"same-file",
"thread_local",
"walkdir",
"winapi-util",
]
[[package]]
name = "indexmap"
version = "1.9.2"
@@ -1821,7 +1847,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.176"
version = "0.0.192"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1841,7 +1867,9 @@ dependencies = [
"fern",
"filetime",
"getrandom 0.2.8",
"glob",
"globset",
"ignore",
"insta",
"itertools",
"libcst",
@@ -1862,6 +1890,7 @@ dependencies = [
"rustpython-parser",
"serde",
"serde_json",
"shellexpand",
"strum",
"strum_macros",
"test-case",
@@ -1869,12 +1898,13 @@ dependencies = [
"titlecase",
"toml",
"update-informer",
"ureq",
"walkdir",
]
[[package]]
name = "ruff_dev"
version = "0.0.176"
version = "0.0.192"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1892,7 +1922,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.176"
version = "0.0.192"
dependencies = [
"proc-macro2",
"quote",
@@ -1935,7 +1965,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
source = "git+https://github.com/RustPython/RustPython.git?rev=c01f014b1269eedcf4bdb45d5fbc62ae2beecf31#c01f014b1269eedcf4bdb45d5fbc62ae2beecf31"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -1945,7 +1975,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
source = "git+https://github.com/RustPython/RustPython.git?rev=c01f014b1269eedcf4bdb45d5fbc62ae2beecf31#c01f014b1269eedcf4bdb45d5fbc62ae2beecf31"
dependencies = [
"ascii",
"cfg-if 1.0.0",
@@ -1968,7 +1998,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
source = "git+https://github.com/RustPython/RustPython.git?rev=c01f014b1269eedcf4bdb45d5fbc62ae2beecf31#c01f014b1269eedcf4bdb45d5fbc62ae2beecf31"
dependencies = [
"bincode",
"bitflags",
@@ -1985,7 +2015,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
source = "git+https://github.com/RustPython/RustPython.git?rev=c01f014b1269eedcf4bdb45d5fbc62ae2beecf31#c01f014b1269eedcf4bdb45d5fbc62ae2beecf31"
dependencies = [
"ahash",
"anyhow",
@@ -2087,6 +2117,15 @@ dependencies = [
"serde",
]
[[package]]
name = "shellexpand"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1c7ddea665294d484c39fd0c0d2b7e35bbfe10035c5fe1854741a57f6880e1"
dependencies = [
"dirs 4.0.0",
]
[[package]]
name = "similar"
version = "2.2.1"
@@ -2297,6 +2336,15 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.1.45"

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.176"
version = "0.0.192"
edition = "2021"
rust-version = "1.65.0"
@@ -28,7 +28,9 @@ common-path = { version = "1.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.17" }
glob = { version = "0.3.0" }
globset = { version = "0.4.9" }
ignore = { version = "0.4.18" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
log = { version = "0.4.17" }
@@ -41,13 +43,14 @@ quick-junit = { version = "0.3.2" }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.176", path = "ruff_macros" }
ruff_macros = { version = "0.0.192", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "c01f014b1269eedcf4bdb45d5fbc62ae2beecf31" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "c01f014b1269eedcf4bdb45d5fbc62ae2beecf31" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "c01f014b1269eedcf4bdb45d5fbc62ae2beecf31" }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" }
shellexpand = { version = "3.0.0" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
textwrap = { version = "0.16.0" }
@@ -69,6 +72,7 @@ assert_cmd = { version = "2.0.4" }
criterion = { version = "0.4.0" }
insta = { version = "1.19.1", features = ["yaml"] }
test-case = { version = "2.2.2" }
ureq = { version = "2.5.0", features = [] }
[features]
default = ["update-informer"]

422
README.md
View File

@@ -39,9 +39,11 @@ Ruff is extremely actively developed and used in major open-source projects like
- [Bokeh](https://github.com/bokeh/bokeh)
- [Zulip](https://github.com/zulip/zulip)
- [Pydantic](https://github.com/pydantic/pydantic)
- [Saleor](https://github.com/saleor/saleor)
- [Hatch](https://github.com/pypa/hatch)
- [Jupyter Server](https://github.com/jupyter-server/jupyter_server)
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Synapse](https://github.com/matrix-org/synapse)
- [Ibis](https://github.com/ibis-project/ibis)
- [Saleor](https://github.com/saleor/saleor)
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -86,6 +88,7 @@ of [Conda](https://docs.conda.io/en/latest/):
1. [flake8-builtins (A)](#flake8-builtins-a)
1. [flake8-comprehensions (C4)](#flake8-comprehensions-c4)
1. [flake8-debugger (T10)](#flake8-debugger-t10)
1. [flake8-errmsg (EM)](#flake8-errmsg-em)
1. [flake8-import-conventions (ICN)](#flake8-import-conventions-icn)
1. [flake8-print (T20)](#flake8-print-t20)
1. [flake8-quotes (Q)](#flake8-quotes-q)
@@ -93,7 +96,9 @@ of [Conda](https://docs.conda.io/en/latest/):
1. [flake8-simplify (SIM)](#flake8-simplify-sim)
1. [flake8-tidy-imports (TID)](#flake8-tidy-imports-tid)
1. [flake8-unused-arguments (ARG)](#flake8-unused-arguments-arg)
1. [flake8-datetimez (DTZ)](#flake8-datetimez-dtz)
1. [eradicate (ERA)](#eradicate-era)
1. [pandas-vet (PD)](#pandas-vet-pd)
1. [pygrep-hooks (PGH)](#pygrep-hooks-pgh)
1. [Pylint (PLC, PLE, PLR, PLW)](#pylint-plc-ple-plr-plw)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf)<!-- End auto-generated table of contents. -->
@@ -116,6 +121,8 @@ Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
pip install ruff
```
[![Packaging status](https://repology.org/badge/vertical-allrepos/ruff-python-linter.svg?exclude_unsupported=1)](https://repology.org/project/ruff-python-linter/versions)
For **macOS Homebrew** and **Linuxbrew** users, Ruff is also available as [`ruff`](https://formulae.brew.sh/formula/ruff) on Homebrew:
```shell
@@ -153,11 +160,13 @@ ruff path/to/code/ --watch
Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.176
hooks:
- id: ruff
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.192'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
args: ["--force-exclude"]
```
## Configuration
@@ -304,13 +313,15 @@ Options:
--per-file-ignores <PER_FILE_IGNORES>
List of mappings from file pattern to code to exclude
--format <FORMAT>
Output serialization format for error messages [default: text] [possible values: text, json, junit, grouped]
Output serialization format for error messages [possible values: text, json, junit, grouped, github]
--show-source
Show violations with source code
--respect-gitignore
Respect file exclusions via `.gitignore` and other standard ignore files
--show-files
See the files Ruff will be run against with the current settings
--show-settings
See Ruff's settings
See the settings Ruff will use to check a given Python file
--add-noqa
Enable automatic additions of noqa directives to failing lines
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
@@ -331,6 +342,55 @@ Options:
Print version information
```
### `pyproject.toml` discovery
Similar to [ESLint](https://eslint.org/docs/latest/user-guide/configuring/configuration-files#cascading-and-hierarchy),
Ruff supports hierarchical configuration, such that the "closest" `pyproject.toml` file in the
directory hierarchy is used for every individual file, with all paths in the `pyproject.toml` file
(e.g., `exclude` globs, `src` paths) being resolved relative to the directory containing the
`pyproject.toml` file.
There are a few exceptions to these rules:
1. In locating the "closest" `pyproject.toml` file for a given path, Ruff ignore any
`pyproject.toml` files that lack a `[tool.ruff]` section.
2. If a configuration file is passed directly via `--config`, those settings are used for across
files. Any relative paths in that configuration file (like `exclude` globs or `src` paths) are
resolved relative to the _current working directory_.
3. If no `pyproject.toml` file is found in the filesystem hierarchy, Ruff will fall back to using
a default configuration. If a user-specific configuration file exists
at `${config_dir}/ruff/pyproject.toml`,
that file will be used instead of the default configuration, with `${config_dir}` being
determined via the [`dirs`](https://docs.rs/dirs/4.0.0/dirs/fn.config_dir.html) crate, and all
relative paths being again resolved relative to the _current working directory_.
4. Any `pyproject.toml`-supported settings that are provided on the command-line (e.g., via
`--select`) will override the settings in _every_ resolved configuration file.
Unlike [ESLint](https://eslint.org/docs/latest/user-guide/configuring/configuration-files#cascading-and-hierarchy),
Ruff does not merge settings across configuration files; instead, the "closest" configuration file
is used, and any parent configuration files are ignored. In lieu of this implicit cascade, Ruff
supports an [`extend`](#extend) field, which allows you to inherit the settings from another
`pyproject.toml` file, like so:
```toml
# Extend the `pyproject.toml` file in the parent directory.
extend = "../pyproject.toml"
# But use a different line length.
line-length = 100
```
### Python file discovery
When passed a path on the command-line, Ruff will automatically discover all Python files in that
path, taking into account the [`exclude`](#exclude) and [`extend-exclude`](#extend-exclude) settings
in each directory's `pyproject.toml` file.
By default, Ruff will also skip any files that are omitted via `.ignore`, `.gitignore`,
`.git/info/exclude`, and global `gitignore` files (see: [`respect-gitignore`](#respect-gitignore)).
Files that are passed to `ruff` directly are always checked, regardless of the above criteria.
For example, `ruff /path/to/excluded/file.py` will always check `file.py`.
### Ignoring errors
To omit a lint check entirely, add it to the "ignore" list via [`ignore`](#ignore) or
@@ -461,6 +521,7 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| E401 | MultipleImportsOnOneLine | Multiple imports on one line | |
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | |
| E501 | LineTooLong | Line too long (89 > 88 characters) | |
| E711 | NoneComparison | Comparison to `None` should be `cond is None` | 🛠 |
@@ -566,6 +627,8 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| UP013 | ConvertTypedDictFunctionalToClass | Convert `...` from `TypedDict` functional to class syntax | 🛠 |
| UP014 | ConvertNamedTupleFunctionalToClass | Convert `...` from `NamedTuple` functional to class syntax | 🛠 |
| UP015 | RedundantOpenModes | Unnecessary open mode parameters | 🛠 |
| UP016 | RemoveSixCompat | Unnecessary `six` compatibility usage | 🛠 |
| UP017 | DatetimeTimezoneUTC | Use `datetime.UTC` alias | 🛠 |
### pep8-naming (N)
@@ -619,7 +682,7 @@ For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/2
| ANN102 | MissingTypeCls | Missing type annotation for `...` in classmethod | |
| ANN201 | MissingReturnTypePublicFunction | Missing return type annotation for public function `...` | |
| ANN202 | MissingReturnTypePrivateFunction | Missing return type annotation for private function `...` | |
| ANN204 | MissingReturnTypeMagicMethod | Missing return type annotation for magic method `...` | |
| ANN204 | MissingReturnTypeSpecialMethod | Missing return type annotation for special method `...` | 🛠 |
| ANN205 | MissingReturnTypeStaticMethod | Missing return type annotation for staticmethod `...` | |
| ANN206 | MissingReturnTypeClassMethod | Missing return type annotation for classmethod `...` | |
| ANN401 | DynamicallyTypedExpression | Dynamically typed expressions (typing.Any) are disallowed in `...` | |
@@ -731,6 +794,16 @@ For more, see [flake8-debugger](https://pypi.org/project/flake8-debugger/4.1.2/)
| ---- | ---- | ------- | --- |
| T100 | Debugger | Import for `...` found | |
### flake8-errmsg (EM)
For more, see [flake8-errmsg](https://pypi.org/project/flake8-errmsg/0.4.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| EM101 | RawStringInException | Exception must not use a string literal, assign to variable first | |
| EM102 | FStringInException | Exception must not use an f-string literal, assign to variable first | |
| EM103 | DotFormatInException | Exception must not use a `.format()` string directly, assign to variable first | |
### flake8-import-conventions (ICN)
| Code | Name | Message | Fix |
@@ -778,7 +851,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| SIM118 | KeyInDict | Use 'key in dict' instead of 'key in dict.keys() | 🛠 |
| SIM118 | KeyInDict | Use `key in dict` instead of `key in dict.keys()` | 🛠 |
### flake8-tidy-imports (TID)
@@ -800,6 +873,22 @@ For more, see [flake8-unused-arguments](https://pypi.org/project/flake8-unused-a
| ARG004 | UnusedStaticMethodArgument | Unused static method argument: `...` | |
| ARG005 | UnusedLambdaArgument | Unused lambda argument: `...` | |
### flake8-datetimez (DTZ)
For more, see [flake8-datetimez](https://pypi.org/project/flake8-datetimez/20.10.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| DTZ001 | CallDatetimeWithoutTzinfo | The use of `datetime.datetime()` without `tzinfo` argument is not allowed | |
| DTZ002 | CallDatetimeToday | The use of `datetime.datetime.today()` is not allowed | |
| DTZ003 | CallDatetimeUtcnow | The use of `datetime.datetime.utcnow()` is not allowed | |
| DTZ004 | CallDatetimeUtcfromtimestamp | The use of `datetime.datetime.utcfromtimestamp()` is not allowed | |
| DTZ005 | CallDatetimeNowWithoutTzinfo | The use of `datetime.datetime.now()` without `tz` argument is not allowed | |
| DTZ006 | CallDatetimeFromtimestamp | The use of `datetime.datetime.fromtimestamp()` without `tz` argument is not allowed | |
| DTZ007 | CallDatetimeStrptimeWithoutZone | The use of `datetime.datetime.strptime()` without %z must be followed by `.replace(tzinfo=)` | |
| DTZ011 | CallDateToday | The use of `datetime.date.today()` is not allowed. | |
| DTZ012 | CallDateFromtimestamp | The use of `datetime.date.fromtimestamp()` is not allowed | |
### eradicate (ERA)
For more, see [eradicate](https://pypi.org/project/eradicate/2.1.0/) on PyPI.
@@ -808,6 +897,25 @@ For more, see [eradicate](https://pypi.org/project/eradicate/2.1.0/) on PyPI.
| ---- | ---- | ------- | --- |
| ERA001 | CommentedOutCode | Found commented-out code | 🛠 |
### pandas-vet (PD)
For more, see [pandas-vet](https://pypi.org/project/pandas-vet/0.2.3/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PD002 | UseOfInplaceArgument | `inplace=True` should be avoided; it has inconsistent behavior | |
| PD003 | UseOfDotIsNull | `.isna` is preferred to `.isnull`; functionality is equivalent | |
| PD004 | UseOfDotNotNull | `.notna` is preferred to `.notnull`; functionality is equivalent | |
| PD007 | UseOfDotIx | `.ix` is deprecated; use more explicit `.loc` or `.iloc` | |
| PD008 | UseOfDotAt | Use `.loc` instead of `.at`. If speed is important, use numpy. | |
| PD009 | UseOfDotIat | Use `.iloc` instead of `.iat`. If speed is important, use numpy. | |
| PD010 | UseOfDotPivotOrUnstack | `.pivot_table` is preferred to `.pivot` or `.unstack`; provides same functionality | |
| PD011 | UseOfDotValues | Use `.to_numpy()` instead of `.values` | |
| PD012 | UseOfDotReadTable | `.read_csv` is preferred to `.read_table`; provides same functionality | |
| PD013 | UseOfDotStack | `.melt` is preferred to `.stack`; provides same functionality | |
| PD015 | UseOfPdMerge | Use `.merge` method instead of `pd.merge` function. They have equivalent functionality. | |
| PD901 | DfIsABadVariableName | `df` is a bad variable name. Be kinder to your future self. | |
### pygrep-hooks (PGH)
For more, see [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) on GitHub.
@@ -815,6 +923,8 @@ For more, see [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) on GitH
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PGH001 | NoEval | No builtin `eval()` allowed | |
| PGH002 | DeprecatedLogWarn | `warn` is deprecated in favor of `warning` | |
| PGH003 | BlanketTypeIgnore | Use specific error codes when ignoring type issues | |
### Pylint (PLC, PLE, PLR, PLW)
@@ -850,7 +960,123 @@ For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
### VS Code (Official)
Download the [Ruff VS Code extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff).
Download the [Ruff VS Code extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff),
which supports autofix actions, import sorting, and more.
![](https://user-images.githubusercontent.com/1309177/205175763-cf34871d-5c05-4abf-9916-440afc82dbf8.gif)
### Language Server Protocol (Official)
Ruff supports the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/)
via the [`ruff-lsp`](https://github.com/charliermarsh/ruff-lsp) Python package, available on
[PyPI](https://pypi.org/project/ruff-lsp/).
[`ruff-lsp`](https://github.com/charliermarsh/ruff-lsp) enables Ruff to be used with any editor that
supports the Language Server Protocol, including [Neovim](https://github.com/charliermarsh/ruff-lsp#example-neovim),
[Sublime Text](https://github.com/charliermarsh/ruff-lsp#example-sublime-text), Emacs, and more.
For example, to use `ruff-lsp` with Neovim, install `ruff-lsp` from PyPI along with
[`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig). Then, add something like the following
to your `init.lua`:
```lua
-- See: https://github.com/neovim/nvim-lspconfig/tree/54eb2a070a4f389b1be0f98070f81d23e2b1a715#suggested-configuration
local opts = { noremap=true, silent=true }
vim.keymap.set('n', '<space>e', vim.diagnostic.open_float, opts)
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, opts)
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, opts)
vim.keymap.set('n', '<space>q', vim.diagnostic.setloclist, opts)
-- Use an on_attach function to only map the following keys
-- after the language server attaches to the current buffer
local on_attach = function(client, bufnr)
-- Enable completion triggered by <c-x><c-o>
vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')
-- Mappings.
-- See `:help vim.lsp.*` for documentation on any of the below functions
local bufopts = { noremap=true, silent=true, buffer=bufnr }
vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, bufopts)
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, bufopts)
vim.keymap.set('n', 'K', vim.lsp.buf.hover, bufopts)
vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, bufopts)
vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, bufopts)
vim.keymap.set('n', '<space>wa', vim.lsp.buf.add_workspace_folder, bufopts)
vim.keymap.set('n', '<space>wr', vim.lsp.buf.remove_workspace_folder, bufopts)
vim.keymap.set('n', '<space>wl', function()
print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
end, bufopts)
vim.keymap.set('n', '<space>D', vim.lsp.buf.type_definition, bufopts)
vim.keymap.set('n', '<space>rn', vim.lsp.buf.rename, bufopts)
vim.keymap.set('n', '<space>ca', vim.lsp.buf.code_action, bufopts)
vim.keymap.set('n', 'gr', vim.lsp.buf.references, bufopts)
vim.keymap.set('n', '<space>f', function() vim.lsp.buf.format { async = true } end, bufopts)
end
-- Configure `ruff-lsp`.
local configs = require 'lspconfig.configs'
if not configs.ruff_lsp then
configs.ruff_lsp = {
default_config = {
cmd = { "ruff-lsp" },
filetypes = {'python'},
root_dir = require('lspconfig').util.find_git_ancestor,
settings = {
ruff_lsp = {
-- Any extra CLI arguments for `ruff` go here.
args = {}
}
}
}
}
end
require('lspconfig').ruff_lsp.setup {
on_attach = on_attach,
}
```
Upon successful installation, you should see Ruff's diagnostics surfaced directly in your editor:
![Code Actions available in Neovim](https://user-images.githubusercontent.com/1309177/208278707-25fa37e4-079d-4597-ad35-b95dba066960.png)
### Language Server Protocol (Unofficial)
Ruff is also available as the [`python-lsp-ruff`](https://github.com/python-lsp/python-lsp-ruff)
plugin for [`python-lsp-server`](https://github.com/python-lsp/python-lsp-ruff), both of which are
installable from PyPI:
```shell
pip install python-lsp-server python-lsp-ruff
```
The LSP server can then be used with any editor that supports the Language Server Protocol.
For example, to use `python-lsp-ruff` with Neovim, add something like the following to your
`init.lua`:
```lua
require'lspconfig'.pylsp.setup {
settings = {
pylsp = {
plugins = {
ruff = {
enabled = true
},
pycodestyle = {
enabled = false
},
pyflakes = {
enabled = false
},
mccabe = {
enabled = false
}
}
}
},
}
```
### PyCharm
@@ -864,10 +1090,13 @@ Ruff should then appear as a runnable action:
![Ruff as a runnable action](https://user-images.githubusercontent.com/1309177/193156026-732b0aaf-3dd9-4549-9b4d-2de6d2168a33.png)
### Vim & Neovim (Unofficial)
### Vim & Neovim
Ruff is available as part of the [coc-pyright](https://github.com/fannheyward/coc-pyright) extension
for coc.nvim.
Ruff can be integrated into any editor that supports the Language Server Protocol via [`ruff-lsp`](https://github.com/charliermarsh/ruff-lsp)
(see: [Language Server Protocol](#language-server-protocol-official)).
Ruff is also available as part of the [coc-pyright](https://github.com/fannheyward/coc-pyright)
extension for `coc.nvim`.
<details>
<summary>Ruff can also be integrated via <a href="https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#efm"><code>efm</code></a> in just a <a href="https://github.com/JafarAbdi/myconfigs/blob/6f0b6b2450e92ec8fc50422928cd22005b919110/efm-langserver/config.yaml#L14-L20">few lines</a>.</summary>
@@ -923,11 +1152,6 @@ null_ls.setup({
</details>
### Language Server Protocol (Unofficial)
[`ruffd`](https://github.com/Seamooo/ruffd) is a Rust-based language server for Ruff that implements
the Language Server Protocol (LSP).
### GitHub Actions
GitHub Actions has everything you need to run Ruff out-of-the-box:
@@ -975,9 +1199,8 @@ Under those conditions, Ruff implements every rule in Flake8.
Ruff also re-implements some of the most popular Flake8 plugins and related code quality tools
natively, including:
- [`isort`](https://pypi.org/project/isort/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
- [`eradicate`](https://pypi.org/project/eradicate/)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
@@ -986,21 +1209,24 @@ natively, including:
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-datetimez`](https://pypi.org/project/flake8-datetimez/)
- [`flake8-debugger`](https://pypi.org/project/flake8-debugger/)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-eradicate`](https://pypi.org/project/flake8-eradicate/)
- [`flake8-errmsg`](https://pypi.org/project/flake8-errmsg/)
- [`flake8-import-conventions`](https://github.com/joaopalmeiro/flake8-import-conventions)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-return`](https://pypi.org/project/flake8-return/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3)
- [`isort`](https://pypi.org/project/isort/)
- [`mccabe`](https://pypi.org/project/mccabe/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (3/10)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (17/33)
- [`yesqa`](https://github.com/asottile/yesqa)
- [`eradicate`](https://pypi.org/project/eradicate/)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33)
- [`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (1/10)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
Note that, in some cases, Ruff uses different error code prefixes than would be found in the
originating Flake8 plugins. For example, Ruff uses `TID252` to represent the `I252` rule from
@@ -1010,8 +1236,7 @@ conflicts with the `isort` rules, like `I001`).
Beyond the rule set, Ruff suffers from the following limitations vis-à-vis Flake8:
1. Ruff does not yet support a few Python 3.9 and 3.10 language features, including structural
pattern matching and parenthesized context managers.
1. Ruff does not yet support structural pattern matching.
2. Flake8 has a plugin architecture and supports writing custom lint rules. (Instead, popular Flake8
plugins are re-implemented in Rust as part of Ruff itself.)
@@ -1031,8 +1256,6 @@ Pylint parity is being tracked in [#689](https://github.com/charliermarsh/ruff/i
Today, Ruff can be used to replace Flake8 when used with any of the following plugins:
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
@@ -1041,9 +1264,11 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-datetimez`](https://pypi.org/project/flake8-datetimez/)
- [`flake8-debugger`](https://pypi.org/project/flake8-debugger/)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-eradicate`](https://pypi.org/project/flake8-eradicate/)
- [`flake8-errmsg`](https://pypi.org/project/flake8-errmsg/)
- [`flake8-import-conventions`](https://github.com/joaopalmeiro/flake8-import-conventions)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
@@ -1051,11 +1276,13 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3)
- [`mccabe`](https://pypi.org/project/mccabe/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
Ruff can also replace [`isort`](https://pypi.org/project/isort/),
[`yesqa`](https://github.com/asottile/yesqa), [`eradicate`](https://pypi.org/project/eradicate/),
[`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (1/10), and a subset of the rules
implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33).
[`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (3/10), and a subset of the rules
implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (17/33).
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.
@@ -1375,7 +1602,7 @@ ignored when evaluating (e.g.) unused-variable checks. The default expression ma
```toml
[tool.ruff]
# Only ignore variables named "_".
dummy_variable_rgx = "^_$"
dummy-variable-rgx = "^_$"
```
---
@@ -1393,7 +1620,7 @@ Exclusions are based on globs, and can be either:
(to exclude any Python files in `directory`). Note that these paths are relative to the
project root (e.g., the directory containing your `pyproject.toml`).
Note that you'll typically want to use [`extend_exclude`](#extend_exclude) to modify
Note that you'll typically want to use [`extend-exclude`](#extend-exclude) to modify
the excluded paths.
**Default value**: `[".bzr", ".direnv", ".eggs", ".git", ".hg", ".mypy_cache", ".nox", ".pants.d", ".ruff_cache", ".svn", ".tox", ".venv", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv"]`
@@ -1409,6 +1636,31 @@ exclude = [".venv"]
---
#### [`extend`](#extend)
A path to a local `pyproject.toml` file to merge into this configuration. User home
directory and environment variables will be expanded.
To resolve the current `pyproject.toml` file, Ruff will first resolve this base
configuration file, then merge in any properties defined in the current configuration
file.
**Default value**: `None`
**Type**: `Path`
**Example usage**:
```toml
[tool.ruff]
# Extend the `pyproject.toml` file in the parent directory.
extend = "../pyproject.toml"
# But use a different line length.
line-length = 100
```
---
#### [`extend-exclude`](#extend-exclude)
A list of file patterns to omit from linting, in addition to those specified by `exclude`.
@@ -1520,6 +1772,30 @@ fixable = ["E", "F"]
---
#### [`force-exclude`](#force-exclude)
Whether to enforce `exclude` and `extend-exclude` patterns, even for paths that are
passed to Ruff explicitly. Typically, Ruff will lint any paths passed in directly, even
if they would typically be excluded. Setting `force-exclude = true` will cause Ruff to
respect these exclusions unequivocally.
This is useful for [`pre-commit`](https://pre-commit.com/), which explicitly passes all
changed files to the [`ruff-pre-commit`](https://github.com/charliermarsh/ruff-pre-commit)
plugin, regardless of whether they're marked as excluded by Ruff's own settings.
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff]
force-exclude = true
```
---
#### [`format`](#format)
The style in which violation messages should be formatted: `"text"` (default),
@@ -1621,6 +1897,24 @@ any matching files.
---
#### [`respect-gitignore`](#respect-gitignore)
Whether to automatically exclude files that are ignored by `.ignore`, `.gitignore`,
`.git/info/exclude`, and global `gitignore` files. Enabled by default.
**Default value**: `true`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff]
respect_gitignore = false
```
---
#### [`select`](#select)
A list of check code prefixes to enable. Prefixes can specify exact checks (like
@@ -1666,6 +1960,26 @@ show-source = true
The source code paths to consider, e.g., when resolving first- vs. third-party imports.
As an example: given a Python package structure like:
```text
my_package/
pyproject.toml
src/
my_package/
__init__.py
foo.py
bar.py
```
The `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.
This 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.
**Default value**: `["."]`
**Type**: `Vec<PathBuf>`
@@ -1815,6 +2129,25 @@ extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
---
### `flake8-errmsg`
#### [`max-string-length`](#max-string-length)
Maximum string length for string literals in exception messages.
**Default value**: `0`
**Type**: `usize`
**Example usage**:
```toml
[tool.ruff.flake8-errmsg]
max-string-length = 20
```
---
### `flake8-import-conventions`
#### [`aliases`](#aliases)
@@ -1951,6 +2284,25 @@ ban-relative-imports = "all"
---
### `flake8-unused-arguments`
#### [`ignore-variadic-names`](#ignore-variadic-names)
Whether to allow unused variadic arguments, like `*args` and `**kwargs`.
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff.flake8-unused-arguments]
ignore-variadic-names = true
```
---
### `isort`
#### [`combine-as-imports`](#combine-as-imports)

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.176"
version = "0.0.192"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.176"
version = "0.0.192"
dependencies = [
"anyhow",
"bincode",
@@ -2028,7 +2028,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
source = "git+https://github.com/RustPython/RustPython.git?rev=c01f014b1269eedcf4bdb45d5fbc62ae2beecf31#c01f014b1269eedcf4bdb45d5fbc62ae2beecf31"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -2038,7 +2038,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
source = "git+https://github.com/RustPython/RustPython.git?rev=c01f014b1269eedcf4bdb45d5fbc62ae2beecf31#c01f014b1269eedcf4bdb45d5fbc62ae2beecf31"
dependencies = [
"ascii",
"cfg-if 1.0.0",
@@ -2061,7 +2061,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
source = "git+https://github.com/RustPython/RustPython.git?rev=c01f014b1269eedcf4bdb45d5fbc62ae2beecf31#c01f014b1269eedcf4bdb45d5fbc62ae2beecf31"
dependencies = [
"bincode",
"bitflags",
@@ -2078,7 +2078,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
source = "git+https://github.com/RustPython/RustPython.git?rev=c01f014b1269eedcf4bdb45d5fbc62ae2beecf31#c01f014b1269eedcf4bdb45d5fbc62ae2beecf31"
dependencies = [
"ahash",
"anyhow",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.176-dev.0"
version = "0.0.192-dev.0"
edition = "2021"
[lib]
@@ -16,6 +16,8 @@ ruff = { path = "..", default-features = false }
rustc-hash = { version = "1.1.0" }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
toml = { version = "0.5.9" }
[dev-dependencies]

View File

@@ -0,0 +1,65 @@
[build-system]
requires = [
# The minimum setuptools version is specific to the PEP 517 backend,
# and may be stricter than the version required in `setup.cfg`
"setuptools>=40.6.0,!=60.9.0",
"wheel",
# Must be kept in sync with the `install_requirements` in `setup.cfg`
"cffi>=1.12; platform_python_implementation != 'PyPy'",
"setuptools-rust>=0.11.4",
]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 79
target-version = ["py36"]
[tool.pytest.ini_options]
addopts = "-r s --capture=no --strict-markers --benchmark-disable"
markers = [
"skip_fips: this test is not executed in FIPS mode",
"supported: parametrized test requiring only_if and skip_message",
]
[tool.mypy]
show_error_codes = true
check_untyped_defs = true
no_implicit_reexport = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_unused_configs = true
strict_equality = true
[[tool.mypy.overrides]]
module = [
"pretend"
]
ignore_missing_imports = true
[tool.coverage.run]
branch = true
relative_files = true
source = [
"cryptography",
"tests/",
]
[tool.coverage.paths]
source = [
"src/cryptography",
"*.tox/*/lib*/python*/site-packages/cryptography",
"*.tox\\*\\Lib\\site-packages\\cryptography",
"*.tox/pypy/site-packages/cryptography",
]
tests =[
"tests/",
"*tests\\",
]
[tool.coverage.report]
exclude_lines = [
"@abc.abstractmethod",
"@abc.abstractproperty",
"@typing.overload",
"if typing.TYPE_CHECKING",
]

View File

@@ -0,0 +1,91 @@
[metadata]
name = cryptography
version = attr: cryptography.__version__
description = cryptography is a package which provides cryptographic recipes and primitives to Python developers.
long_description = file: README.rst
long_description_content_type = text/x-rst
license = BSD-3-Clause OR Apache-2.0
url = https://github.com/pyca/cryptography
author = The Python Cryptographic Authority and individual contributors
author_email = cryptography-dev@python.org
project_urls =
Documentation=https://cryptography.io/
Source=https://github.com/pyca/cryptography/
Issues=https://github.com/pyca/cryptography/issues
Changelog=https://cryptography.io/en/latest/changelog/
classifiers =
Development Status :: 5 - Production/Stable
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
License :: OSI Approved :: BSD License
Natural Language :: English
Operating System :: MacOS :: MacOS X
Operating System :: POSIX
Operating System :: POSIX :: BSD
Operating System :: POSIX :: Linux
Operating System :: Microsoft :: Windows
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
Topic :: Security :: Cryptography
[options]
python_requires = >=3.6
include_package_data = True
zip_safe = False
package_dir =
=src
packages = find:
# `install_requires` must be kept in sync with `pyproject.toml`
install_requires =
cffi >=1.12
[options.packages.find]
where = src
exclude =
_cffi_src
_cffi_src.*
[options.extras_require]
test =
pytest>=6.2.0
pytest-benchmark
pytest-cov
pytest-subtests
pytest-xdist
pretend
iso8601
pytz
hypothesis>=1.11.4,!=3.79.2
docs =
sphinx >= 1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0
sphinx_rtd_theme
docstest =
pyenchant >= 1.6.11
twine >= 1.12.0
sphinxcontrib-spelling >= 4.0.1
sdist =
setuptools_rust >= 0.11.4
pep8test =
black
flake8
flake8-import-order
pep8-naming
# This extra is for OpenSSH private keys that use bcrypt KDF
# Versions: v3.1.3 - ignore_few_rounds, v3.1.5 - abi3
ssh =
bcrypt >= 3.1.5
[flake8]
ignore = E203,E211,W503,W504,N818
exclude = .tox,*.egg,.git,_build,.hypothesis
select = E,W,F,N,I
application-import-names = cryptography,cryptography_vectors,tests

View File

@@ -0,0 +1,32 @@
//! Extract Black configuration settings from a pyproject.toml.
use std::path::Path;
use anyhow::Result;
use ruff::settings::types::PythonVersion;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Black {
#[serde(alias = "line-length", alias = "line_length")]
pub line_length: Option<usize>,
#[serde(alias = "target-version", alias = "target_version")]
pub target_version: Option<Vec<PythonVersion>>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Tools {
black: Option<Black>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Pyproject {
tool: Option<Tools>,
}
pub fn parse_black_options<P: AsRef<Path>>(path: P) -> Result<Option<Black>> {
let contents = std::fs::read_to_string(path)?;
Ok(toml::from_str::<Pyproject>(&contents)?
.tool
.and_then(|tool| tool.black))
}

View File

@@ -7,16 +7,24 @@ use ruff::flake8_tidy_imports::settings::Strictness;
use ruff::settings::options::Options;
use ruff::settings::pyproject::Pyproject;
use ruff::{
flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming,
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_quotes, flake8_tidy_imports, mccabe,
pep8_naming,
};
use crate::black::Black;
use crate::plugin::Plugin;
use crate::{parser, plugin};
pub fn convert(
flake8: &HashMap<String, Option<String>>,
config: &HashMap<String, HashMap<String, Option<String>>>,
black: Option<&Black>,
plugins: Option<Vec<Plugin>>,
) -> Result<Pyproject> {
// Extract the Flake8 section.
let flake8 = config
.get("flake8")
.expect("Unable to find flake8 section in INI file");
// Extract all referenced check code prefixes, to power plugin inference.
let mut referenced_codes: BTreeSet<CheckCodePrefix> = BTreeSet::default();
for (key, value) in flake8 {
@@ -53,10 +61,18 @@ pub fn convert(
plugin::resolve_select(
flake8,
&plugins.unwrap_or_else(|| {
plugin::infer_plugins_from_options(flake8)
.into_iter()
.chain(plugin::infer_plugins_from_codes(&referenced_codes))
.collect()
let from_options = plugin::infer_plugins_from_options(flake8);
if !from_options.is_empty() {
eprintln!("Inferred plugins from settings: {from_options:#?}");
}
let from_codes = plugin::infer_plugins_from_codes(&referenced_codes);
if !from_codes.is_empty() {
eprintln!(
"Inferred plugins from referenced check codes: {:#?}",
from_codes
);
}
from_options.into_iter().chain(from_codes).collect()
}),
)
});
@@ -73,6 +89,7 @@ pub fn convert(
let mut options = Options::default();
let mut flake8_annotations = flake8_annotations::settings::Options::default();
let mut flake8_bugbear = flake8_bugbear::settings::Options::default();
let mut flake8_errmsg = flake8_errmsg::settings::Options::default();
let mut flake8_quotes = flake8_quotes::settings::Options::default();
let mut flake8_tidy_imports = flake8_tidy_imports::settings::Options::default();
let mut mccabe = mccabe::settings::Options::default();
@@ -194,6 +211,15 @@ pub fn convert(
Ok(max_complexity) => mccabe.max_complexity = Some(max_complexity),
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
},
// flake8-errmsg
"errmsg-max-string-length" | "errmsg_max_string_length" => {
match value.clone().parse::<usize>() {
Ok(max_string_length) => {
flake8_errmsg.max_string_length = Some(max_string_length);
}
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
}
}
// Unknown
_ => eprintln!("Skipping unsupported property: {key}"),
}
@@ -209,6 +235,9 @@ pub fn convert(
if flake8_bugbear != flake8_bugbear::settings::Options::default() {
options.flake8_bugbear = Some(flake8_bugbear);
}
if flake8_errmsg != flake8_errmsg::settings::Options::default() {
options.flake8_errmsg = Some(flake8_errmsg);
}
if flake8_quotes != flake8_quotes::settings::Options::default() {
options.flake8_quotes = Some(flake8_quotes);
}
@@ -222,6 +251,19 @@ pub fn convert(
options.pep8_naming = Some(pep8_naming);
}
// Extract any settings from the existing `pyproject.toml`.
if let Some(black) = black {
if let Some(line_length) = &black.line_length {
options.line_length = Some(*line_length);
}
if let Some(target_version) = &black.target_version {
if let Some(target_version) = target_version.iter().min() {
options.target_version = Some(*target_version);
}
}
}
// Create the pyproject.toml.
Ok(Pyproject::new(options))
}
@@ -241,11 +283,16 @@ mod tests {
#[test]
fn it_converts_empty() -> Result<()> {
let actual = convert(&HashMap::from([]), None)?;
let actual = convert(
&HashMap::from([("flake8".to_string(), HashMap::default())]),
None,
None,
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -253,10 +300,12 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -268,9 +317,11 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -284,13 +335,18 @@ mod tests {
#[test]
fn it_converts_dashes() -> Result<()> {
let actual = convert(
&HashMap::from([("max-line-length".to_string(), Some("100".to_string()))]),
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("max-line-length".to_string(), Some("100".to_string()))]),
)]),
None,
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -298,10 +354,12 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: Some(100),
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -313,9 +371,11 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -329,13 +389,18 @@ mod tests {
#[test]
fn it_converts_underscores() -> Result<()> {
let actual = convert(
&HashMap::from([("max_line_length".to_string(), Some("100".to_string()))]),
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("max_line_length".to_string(), Some("100".to_string()))]),
)]),
None,
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -343,10 +408,12 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: Some(100),
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -358,9 +425,11 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -374,13 +443,18 @@ mod tests {
#[test]
fn it_ignores_parse_errors() -> Result<()> {
let actual = convert(
&HashMap::from([("max_line_length".to_string(), Some("abc".to_string()))]),
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("max_line_length".to_string(), Some("abc".to_string()))]),
)]),
None,
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -388,10 +462,12 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -403,9 +479,11 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -419,13 +497,18 @@ mod tests {
#[test]
fn it_converts_plugin_options() -> Result<()> {
let actual = convert(
&HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
)]),
None,
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -433,10 +516,12 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -448,6 +533,7 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
@@ -456,6 +542,7 @@ mod tests {
}),
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -470,15 +557,20 @@ mod tests {
fn it_converts_docstring_conventions() -> Result<()> {
let actual = convert(
&HashMap::from([(
"docstring-convention".to_string(),
Some("numpy".to_string()),
"flake8".to_string(),
HashMap::from([(
"docstring-convention".to_string(),
Some("numpy".to_string()),
)]),
)]),
None,
Some(vec![Plugin::Flake8Docstrings]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -486,10 +578,12 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::D100,
CheckCodePrefix::D101,
@@ -537,9 +631,11 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -553,13 +649,18 @@ mod tests {
#[test]
fn it_infers_plugins_if_omitted() -> Result<()> {
let actual = convert(
&HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
)]),
None,
None,
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -567,10 +668,12 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -583,6 +686,7 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
@@ -591,6 +695,7 @@ mod tests {
}),
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,

View File

@@ -11,6 +11,7 @@
clippy::too_many_lines
)]
pub mod black;
pub mod converter;
mod parser;
pub mod plugin;

View File

@@ -17,6 +17,7 @@ use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use configparser::ini::Ini;
use flake8_to_ruff::black::parse_black_options;
use flake8_to_ruff::converter;
use flake8_to_ruff::plugin::Plugin;
@@ -26,10 +27,14 @@ use flake8_to_ruff::plugin::Plugin;
long_about = None
)]
struct Cli {
/// Path to the Flake8 configuration file (e.g., 'setup.cfg', 'tox.ini', or
/// '.flake8').
/// Path to the Flake8 configuration file (e.g., `setup.cfg`, `tox.ini`, or
/// `.flake8`).
#[arg(required = true)]
file: PathBuf,
/// Optional path to a `pyproject.toml` file, used to ensure compatibility
/// with Black.
#[arg(long)]
pyproject: Option<PathBuf>,
/// List of plugins to enable.
#[arg(long, value_delimiter = ',')]
plugin: Option<Vec<Plugin>>,
@@ -43,13 +48,15 @@ fn main() -> Result<()> {
ini.set_multiline(true);
let config = ini.load(cli.file).map_err(|msg| anyhow::anyhow!(msg))?;
// Extract the Flake8 section.
let flake8 = config
.get("flake8")
.expect("Unable to find flake8 section in INI file");
// Read the pyproject.toml file.
let black = cli
.pyproject
.map(parse_black_options)
.transpose()?
.flatten();
// Create the pyproject.toml.
let pyproject = converter::convert(flake8, cli.plugin)?;
// Create Ruff's pyproject.toml section.
let pyproject = converter::convert(&config, black.as_ref(), cli.plugin)?;
println!("{}", toml::to_string_pretty(&pyproject)?);
Ok(())

View File

@@ -3,6 +3,7 @@ use std::str::FromStr;
use anyhow::{bail, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use ruff::checks::PREFIX_REDIRECTS;
use ruff::checks_gen::CheckCodePrefix;
use ruff::settings::types::PatternPrefixPair;
use rustc_hash::FxHashMap;
@@ -18,7 +19,9 @@ pub fn parse_prefix_codes(value: &str) -> Vec<CheckCodePrefix> {
if code.is_empty() {
continue;
}
if let Ok(code) = CheckCodePrefix::from_str(code) {
if let Some(code) = PREFIX_REDIRECTS.get(code) {
codes.push(code.clone());
} else if let Ok(code) = CheckCodePrefix::from_str(code) {
codes.push(code);
} else {
eprintln!("Unsupported prefix code: {code}");
@@ -83,16 +86,22 @@ impl State {
fn parse(&self) -> Vec<PatternPrefixPair> {
let mut codes: Vec<PatternPrefixPair> = vec![];
for code in &self.codes {
match CheckCodePrefix::from_str(code) {
Ok(code) => {
for filename in &self.filenames {
codes.push(PatternPrefixPair {
pattern: filename.clone(),
prefix: code.clone(),
});
}
if let Some(code) = PREFIX_REDIRECTS.get(code.as_str()) {
for filename in &self.filenames {
codes.push(PatternPrefixPair {
pattern: filename.clone(),
prefix: code.clone(),
});
}
Err(_) => eprintln!("Skipping unrecognized prefix: {code}"),
} else if let Ok(code) = CheckCodePrefix::from_str(code) {
for filename in &self.filenames {
codes.push(PatternPrefixPair {
pattern: filename.clone(),
prefix: code.clone(),
});
}
} else {
eprintln!("Unsupported prefix code: {code}");
}
}
codes

View File

@@ -1,10 +1,11 @@
use std::collections::{BTreeSet, HashMap};
use std::fmt;
use std::str::FromStr;
use anyhow::anyhow;
use ruff::checks_gen::CheckCodePrefix;
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Plugin {
Flake8Annotations,
Flake8Bandit,
@@ -12,8 +13,10 @@ pub enum Plugin {
Flake8Bugbear,
Flake8Builtins,
Flake8Comprehensions,
Flake8Datetimez,
Flake8Debugger,
Flake8Docstrings,
Flake8ErrMsg,
Flake8Eradicate,
Flake8Print,
Flake8Quotes,
@@ -21,6 +24,7 @@ pub enum Plugin {
Flake8Simplify,
Flake8TidyImports,
McCabe,
PandasVet,
PEP8Naming,
Pyupgrade,
}
@@ -36,15 +40,18 @@ impl FromStr for Plugin {
"flake8-bugbear" => Ok(Plugin::Flake8Bugbear),
"flake8-builtins" => Ok(Plugin::Flake8Builtins),
"flake8-comprehensions" => Ok(Plugin::Flake8Comprehensions),
"flake8-datetimez" => Ok(Plugin::Flake8Datetimez),
"flake8-debugger" => Ok(Plugin::Flake8Debugger),
"flake8-docstrings" => Ok(Plugin::Flake8Docstrings),
"flake8-eradicate" => Ok(Plugin::Flake8BlindExcept),
"flake8-eradicate" => Ok(Plugin::Flake8Eradicate),
"flake8-errmsg" => Ok(Plugin::Flake8ErrMsg),
"flake8-print" => Ok(Plugin::Flake8Print),
"flake8-quotes" => Ok(Plugin::Flake8Quotes),
"flake8-return" => Ok(Plugin::Flake8Return),
"flake8-simplify" => Ok(Plugin::Flake8Simplify),
"flake8-tidy-imports" => Ok(Plugin::Flake8TidyImports),
"mccabe" => Ok(Plugin::McCabe),
"pandas-vet" => Ok(Plugin::PandasVet),
"pep8-naming" => Ok(Plugin::PEP8Naming),
"pyupgrade" => Ok(Plugin::Pyupgrade),
_ => Err(anyhow!("Unknown plugin: {string}")),
@@ -52,26 +59,62 @@ impl FromStr for Plugin {
}
}
impl fmt::Debug for Plugin {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
Plugin::Flake8Annotations => "flake8-annotations",
Plugin::Flake8Bandit => "flake8-bandit",
Plugin::Flake8BlindExcept => "flake8-blind-except",
Plugin::Flake8Bugbear => "flake8-bugbear",
Plugin::Flake8Builtins => "flake8-builtins",
Plugin::Flake8Comprehensions => "flake8-comprehensions",
Plugin::Flake8Datetimez => "flake8-datetimez",
Plugin::Flake8Debugger => "flake8-debugger",
Plugin::Flake8Docstrings => "flake8-docstrings",
Plugin::Flake8Eradicate => "flake8-eradicate",
Plugin::Flake8ErrMsg => "flake8-errmsg",
Plugin::Flake8Print => "flake8-print",
Plugin::Flake8Quotes => "flake8-quotes",
Plugin::Flake8Return => "flake8-return",
Plugin::Flake8Simplify => "flake8-simplify",
Plugin::Flake8TidyImports => "flake8-tidy-imports",
Plugin::McCabe => "mccabe",
Plugin::PandasVet => "pandas-vet",
Plugin::PEP8Naming => "pep8-naming",
Plugin::Pyupgrade => "pyupgrade",
}
)
}
}
impl Plugin {
pub fn default(&self) -> CheckCodePrefix {
match self {
Plugin::Flake8Annotations => CheckCodePrefix::ANN,
Plugin::Flake8Bandit => CheckCodePrefix::S,
// TODO(charlie): Handle rename of `B` to `BLE`.
Plugin::Flake8BlindExcept => CheckCodePrefix::BLE,
Plugin::Flake8Bugbear => CheckCodePrefix::B,
Plugin::Flake8Builtins => CheckCodePrefix::A,
Plugin::Flake8Comprehensions => CheckCodePrefix::C4,
Plugin::Flake8Datetimez => CheckCodePrefix::DTZ,
Plugin::Flake8Debugger => CheckCodePrefix::T1,
Plugin::Flake8Docstrings => CheckCodePrefix::D,
// TODO(charlie): Handle rename of `E` to `ERA`.
Plugin::Flake8Eradicate => CheckCodePrefix::ERA,
Plugin::Flake8ErrMsg => CheckCodePrefix::EM,
Plugin::Flake8Print => CheckCodePrefix::T2,
Plugin::Flake8Quotes => CheckCodePrefix::Q,
Plugin::Flake8Return => CheckCodePrefix::RET,
Plugin::Flake8Simplify => CheckCodePrefix::SIM,
Plugin::Flake8TidyImports => CheckCodePrefix::I25,
Plugin::Flake8TidyImports => CheckCodePrefix::TID25,
Plugin::McCabe => CheckCodePrefix::C9,
Plugin::PandasVet => CheckCodePrefix::PD,
Plugin::PEP8Naming => CheckCodePrefix::N,
Plugin::Pyupgrade => CheckCodePrefix::U,
Plugin::Pyupgrade => CheckCodePrefix::UP,
}
}
@@ -83,6 +126,7 @@ impl Plugin {
Plugin::Flake8Bugbear => vec![CheckCodePrefix::B],
Plugin::Flake8Builtins => vec![CheckCodePrefix::A],
Plugin::Flake8Comprehensions => vec![CheckCodePrefix::C4],
Plugin::Flake8Datetimez => vec![CheckCodePrefix::DTZ],
Plugin::Flake8Debugger => vec![CheckCodePrefix::T1],
Plugin::Flake8Docstrings => {
// Use the user-provided docstring.
@@ -101,14 +145,16 @@ impl Plugin {
DocstringConvention::PEP8.select()
}
Plugin::Flake8Eradicate => vec![CheckCodePrefix::ERA],
Plugin::Flake8ErrMsg => vec![CheckCodePrefix::EM],
Plugin::Flake8Print => vec![CheckCodePrefix::T2],
Plugin::Flake8Quotes => vec![CheckCodePrefix::Q],
Plugin::Flake8Return => vec![CheckCodePrefix::RET],
Plugin::Flake8Simplify => vec![CheckCodePrefix::SIM],
Plugin::Flake8TidyImports => vec![CheckCodePrefix::I25],
Plugin::Flake8TidyImports => vec![CheckCodePrefix::TID],
Plugin::McCabe => vec![CheckCodePrefix::C9],
Plugin::PandasVet => vec![CheckCodePrefix::PD],
Plugin::PEP8Naming => vec![CheckCodePrefix::N],
Plugin::Pyupgrade => vec![CheckCodePrefix::U],
Plugin::Pyupgrade => vec![CheckCodePrefix::UP],
}
}
}
@@ -377,6 +423,9 @@ pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> V
"staticmethod-decorators" | "staticmethod_decorators" => {
plugins.insert(Plugin::PEP8Naming);
}
"max-string-length" | "max_string_length" => {
plugins.insert(Plugin::Flake8ErrMsg);
}
_ => {}
}
}
@@ -395,16 +444,18 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin
Plugin::Flake8Bugbear,
Plugin::Flake8Builtins,
Plugin::Flake8Comprehensions,
Plugin::Flake8Datetimez,
Plugin::Flake8Debugger,
Plugin::Flake8Docstrings,
Plugin::Flake8Eradicate,
Plugin::Flake8ErrMsg,
Plugin::Flake8Print,
Plugin::Flake8Quotes,
Plugin::Flake8Return,
Plugin::Flake8Simplify,
Plugin::Flake8TidyImports,
Plugin::PandasVet,
Plugin::PEP8Naming,
Plugin::Pyupgrade,
]
.into_iter()
.filter(|plugin| {

View File

@@ -32,6 +32,8 @@ build-backend = "maturin"
bindings = "bin"
strip = true
[tool.ruff]
[tool.ruff.isort]
force-wrap-aliases = true
combine-as-imports = true

3
resources/test/fixtures/README.md vendored Normal file
View File

@@ -0,0 +1,3 @@
# fixtures
Fixture files used for snapshot testing.

View File

@@ -55,3 +55,5 @@ a.get("hello", False)
{}.pop(True, False)
dict.fromkeys(("world",), True)
{}.deploy(True, False)
getattr(someobj, attrname, False)
mylist.index(True)

View File

@@ -20,6 +20,8 @@ getattr(foo, "_123abc")
getattr(foo, "abc123")
getattr(foo, r"abc123")
_ = lambda x: getattr(x, "bar")
if getattr(x, "bar"):
pass
# Valid setattr usage
setattr(foo, bar, None)
@@ -28,6 +30,8 @@ setattr(foo, "123abc", None)
setattr(foo, r"123\abc", None)
setattr(foo, "except", None)
_ = lambda x: setattr(x, "bar", 1)
if setattr(x, "bar", 1):
pass
# Invalid usage
setattr(foo, "bar", None)

View File

@@ -0,0 +1,21 @@
import datetime
# no args
datetime.datetime(2000, 1, 1, 0, 0, 0)
# none args
datetime.datetime(2000, 1, 1, 0, 0, 0, 0, None)
# not none arg
datetime.datetime(2000, 1, 1, 0, 0, 0, 0, datetime.timezone.utc)
# no kwargs
datetime.datetime(2000, 1, 1, fold=1)
# none kwargs
datetime.datetime(2000, 1, 1, tzinfo=None)
from datetime import datetime
# no args unqualified
datetime(2000, 1, 1, 0, 0, 0)

View File

@@ -0,0 +1,9 @@
import datetime
# qualified
datetime.datetime.today()
from datetime import datetime
# unqualified
datetime.today()

View File

@@ -0,0 +1,9 @@
import datetime
# qualified
datetime.datetime.utcnow()
from datetime import datetime
# unqualified
datetime.utcnow()

View File

@@ -0,0 +1,9 @@
import datetime
# qualified
datetime.datetime.utcfromtimestamp(1234)
from datetime import datetime
# unqualified
datetime.utcfromtimestamp(1234)

View File

@@ -0,0 +1,18 @@
import datetime
# no args
datetime.datetime.now()
# wrong keywords
datetime.datetime.now(bad=datetime.timezone.utc)
# none args
datetime.datetime.now(None)
# none keywords
datetime.datetime.now(tz=None)
from datetime import datetime
# no args unqualified
datetime.now()

View File

@@ -0,0 +1,18 @@
import datetime
# no args
datetime.datetime.fromtimestamp(1234)
# wrong keywords
datetime.datetime.fromtimestamp(1234, bad=datetime.timezone.utc)
# none args
datetime.datetime.fromtimestamp(1234, None)
# none keywords
datetime.datetime.fromtimestamp(1234, tz=None)
from datetime import datetime
# no args unqualified
datetime.fromtimestamp(1234)

View File

@@ -0,0 +1,35 @@
import datetime
# bad format
datetime.datetime.strptime("something", "%H:%M:%S%Z")
# no replace or astimezone
datetime.datetime.strptime("something", "something")
# wrong replace
datetime.datetime.strptime("something", "something").replace(hour=1)
# none replace
datetime.datetime.strptime("something", "something").replace(tzinfo=None)
# OK
datetime.datetime.strptime("something", "something").replace(
tzinfo=datetime.timezone.utc
)
# OK
datetime.datetime.strptime("something", "something").astimezone()
# OK
datetime.datetime.strptime("something", "%H:%M:%S%z")
# OK
datetime.datetime.strptime("something", something).astimezone()
# OK
datetime.datetime.strptime("something", something).replace(tzinfo=datetime.timezone.utc)
from datetime import datetime
# no replace orastimezone unqualified
datetime.strptime("something", "something")

View File

@@ -0,0 +1,9 @@
import datetime
# qualified
datetime.date.today()
from datetime import date
# unqualified
date.today()

View File

@@ -0,0 +1,9 @@
import datetime
# qualified
datetime.date.fromtimestamp(1234)
from datetime import date
# unqualified
date.fromtimestamp(1234)

View File

@@ -1,6 +1,5 @@
breakpoint()
import pdb
import builtins
from builtins import breakpoint
@@ -9,7 +8,6 @@ from celery.contrib.rdb import set_trace
from celery.contrib import rdb
import celery.contrib.rdb
breakpoint()
st()
set_trace()

View File

@@ -0,0 +1,23 @@
from __future__ import annotations
def f_a():
raise RuntimeError("This is an example exception")
def f_a_short():
raise RuntimeError("Error")
def f_b():
example = "example"
raise RuntimeError(f"This is an {example} exception")
def f_c():
raise RuntimeError("This is an {example} exception".format(example="example"))
def f_ok():
msg = "hello"
raise RuntimeError(msg)

View File

@@ -1 +1,10 @@
import sys
import tempfile
print("Hello, world!") # T201
print("Hello, world!", file=None) # T201
print("Hello, world!", file=sys.stdout) # T201
print("Hello, world!", file=sys.stderr) # T201
with tempfile.NamedTemporaryFile() as fp:
print("Hello, world!", file=fp) # OK

View File

@@ -2,7 +2,6 @@ from pprint import pprint
pprint("Hello, world!") # T203
import pprint
pprint.pprint("Hello, world!") # T203

View File

@@ -6,18 +6,6 @@ def x():
return a # error
def x():
b, a = 1, 2
print(b)
return a # error
def x():
a = 1
print()
return a # error
def x():
a = 1
print(a)
@@ -53,7 +41,6 @@ def x():
# https://github.com/afonasev/flake8-return/issues/47#issue-641117366
def user_agent_username(username=None):
if not username:
return ""
@@ -136,6 +123,20 @@ def x():
return a
# Considered OK, since functions can have side effects.
def x():
b, a = 1, 2
print(b)
return a
# Considered OK, since functions can have side effects.
def x():
a = 1
print()
return a
# Test cases for using value for assignment then returning it
# See:https://github.com/afonasev/flake8-return/issues/47
def resolve_from_url(self, url: str) -> dict:

View File

@@ -27,7 +27,7 @@ def f(cls, x):
lambda x: print("Hello, world!")
class X:
class C:
###
# Unused arguments.
###

View File

@@ -0,0 +1,14 @@
def f(a, b):
print("Hello, world!")
def f(a, b, *args, **kwargs):
print("Hello, world!")
class C:
def f(self, a, b):
print("Hello, world!")
def f(self, a, b, *args, **kwargs):
print("Hello, world!")

View File

@@ -39,3 +39,27 @@ if True:
import collections
import typing
def f(): pass
import os
# Comment goes here.
def f():
pass
import os
# Comment goes here.
def f():
pass
import os
# Comment goes here.
# And another.
def f():
pass

View File

@@ -39,3 +39,27 @@ if True:
import collections
import typing
def f(): pass
import os
# Comment goes here.
def f():
pass
import os
# Comment goes here.
def f():
pass
import os
# Comment goes here.
# And another.
def f():
pass

View File

@@ -4,3 +4,10 @@ import os
if True:
x = 1; import sys
import os
if True:
x = 1; \
import os
x = 1; \
import os

View File

@@ -0,0 +1,61 @@
#: E401
import os, sys
#: Okay
import os
import sys
from subprocess import Popen, PIPE
from myclass import MyClass
from foo.bar.yourclass import YourClass
import myclass
import foo.bar.yourclass
#: Okay
__all__ = ['abc']
import foo
#: Okay
__version__ = "42"
import foo
#: Okay
__author__ = "Simon Gomizelj"
import foo
#: Okay
try:
import foo
except ImportError:
pass
else:
print('imported foo')
finally:
print('made attempt to import foo')
import bar
#: Okay
with warnings.catch_warnings():
warnings.filterwarnings("ignore", DeprecationWarning)
import foo
import bar
#: Okay
if False:
import foo
elif not True:
import bar
else:
import mwahaha
import bar
#: E402
VERSION = '1.2.3'
import foo
#: E402
import foo
a = 1
import bar

View File

@@ -55,3 +55,8 @@ sit amet consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labor
# OK
# https://loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong.url.com
# Not OK
_ = """
Source: https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
"""

View File

@@ -0,0 +1,39 @@
###
# Errors
###
if "abc" is "def": # F632 (fix)
pass
if "abc" is None: # F632 (fix, but leaves behind unfixable E711)
pass
if None is "abc": # F632 (fix, but leaves behind unfixable E711)
pass
if "abc" is False: # F632 (fix, but leaves behind unfixable E712)
pass
if False is "abc": # F632 (fix, but leaves behind unfixable E712)
pass
if False == None: # E711, E712 (fix)
pass
if None == False: # E711, E712 (fix)
pass
###
# Unfixable errors
###
if "abc" == None: # E711
pass
if None == "abc": # E711
pass
if "abc" == False: # E712
pass
if False == "abc": # E712
pass
###
# Non-errors
###
if "def" == "abc":
pass
if False is None:
pass
if None is False:
pass

View File

@@ -0,0 +1,16 @@
"""Test: noqa directives."""
from module import (
A, # noqa: F401
B,
)
from module import (
A, # noqa: F401
B, # noqa: F401
)
from module import (
A,
B,
)

View File

@@ -7,7 +7,6 @@ import fu
class bar:
# STOPSHIP: This errors.
fu = 1

View File

@@ -0,0 +1,13 @@
"""Test: Mypy extensions."""
from mypy_extensions import DefaultNamedArg
# OK
_ = DefaultNamedArg(bool | None, name="some_prop_name")
_ = DefaultNamedArg(type=bool | None, name="some_prop_name")
_ = DefaultNamedArg(bool | None, "some_prop_name")
# Not OK
_ = DefaultNamedArg("Undefined", name="some_prop_name")
_ = DefaultNamedArg(type="Undefined", name="some_prop_name")
_ = DefaultNamedArg("Undefined", "some_prop_name")

View File

@@ -10,13 +10,13 @@ except ValueError as e:
print(e)
def f1():
def f():
x = 1
y = 2
z = x + y
def f2():
def f():
foo = (1, 2)
(a, b) = (1, 2)
@@ -26,12 +26,12 @@ def f2():
(x, y) = baz = bar
def f3():
def f():
locals()
x = 1
def f4():
def f():
_ = 1
__ = 1
_discarded = 1
@@ -40,26 +40,26 @@ def f4():
a = 1
def f5():
def f():
global a
# Used in `f7` via `nonlocal`.
# Used in `c` via `nonlocal`.
b = 1
def f6():
def c():
# F841
b = 1
def f7():
def d():
nonlocal b
def f6():
def f():
annotations = []
assert len([annotations for annotations in annotations])
def f7():
def f():
def connect():
return None, None
@@ -67,6 +67,22 @@ def f7():
cursor.execute("SELECT * FROM users")
def f8():
with open("file") as f, open("") as ((a, b)):
def f():
def connect():
return None, None
with (connect() as (connection, cursor)):
cursor.execute("SELECT * FROM users")
def f():
with open("file") as my_file, open("") as ((this, that)):
print("hello")
def f():
with (
open("file") as my_file,
open("") as ((this, that)),
):
print("hello")

View File

@@ -0,0 +1,51 @@
if True:
import foo; x = 1
import foo; x = 1
if True:
import foo; \
x = 1
if True:
import foo \
; x = 1
if True:
x = 1; import foo
if True:
x = 1; \
import foo
if True:
x = 1 \
; import foo
if True:
x = 1; import foo; x = 1
x = 1; import foo; x = 1
if True:
x = 1; \
import foo; \
x = 1
if True:
x = 1 \
;import foo \
;x = 1
# Continuation, but not as the last content in the file.
x = 1; \
import foo
# Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax
# error.)
x = 1; \
import foo

View File

@@ -0,0 +1,8 @@
import logging
import warnings
from warnings import warn
warnings.warn("this is ok")
warn("by itself is also ok")
logging.warning("this is fine")
log.warning("this is ok")

View File

@@ -0,0 +1,15 @@
import logging
from logging import warn
logging.warn("this is not ok")
log.warn("this is also not ok")
warn("not ok")
def foo():
from logging import warn
def warn():
pass
warn("has been redefined, but we will still report it")

View File

@@ -0,0 +1,11 @@
x = 1 # type: ignore
x = 1 # type ignore
x = 1 # type:ignore
x = 1
x = 1 # type ignore # noqa
x = 1 # type: ignore[attr-defined]
x = 1 # type: ignore[attr-defined, name-defined]
x = 1 # type: ignore[type-mismatch] # noqa
x = 1 # type: Union[int, str]
x = 1 # type: ignoreme

View File

@@ -109,6 +109,11 @@ def f():
del x
def f():
print(f"{x=}")
global x
###
# Non-errors.
###
@@ -146,3 +151,8 @@ def f():
global x, y
del x
def f():
global x
print(f"{x=}")

View File

@@ -42,6 +42,9 @@ staticmethod-decorators = ["staticmethod"]
[tool.ruff.flake8-tidy-imports]
ban-relative-imports = "parents"
[tool.ruff.flake8-errmsg]
max-string-length = 20
[tool.ruff.flake8-import-conventions.aliases]
pandas = "pd"

View File

@@ -0,0 +1,87 @@
# Replace names by built-in names, whether namespaced or not
# https://github.com/search?q=%22from+six+import%22&type=code
import six
from six.moves import map # No need
from six import text_type
six.text_type # str
six.binary_type # bytes
six.class_types # (type,)
six.string_types # (str,)
six.integer_types # (int,)
six.unichr # chr
six.iterbytes # iter
six.print_(...) # print(...)
six.exec_(c, g, l) # exec(c, g, l)
six.advance_iterator(it) # next(it)
six.next(it) # next(it)
six.callable(x) # callable(x)
six.moves.range(x) # range(x)
six.moves.xrange(x) # range(x)
isinstance(..., six.class_types) # isinstance(..., type)
issubclass(..., six.integer_types) # issubclass(..., int)
isinstance(..., six.string_types) # isinstance(..., str)
# Replace call on arg by method call on arg
six.iteritems(dct) # dct.items()
six.iterkeys(dct) # dct.keys()
six.itervalues(dct) # dct.values()
six.viewitems(dct) # dct.items()
six.viewkeys(dct) # dct.keys()
six.viewvalues(dct) # dct.values()
six.assertCountEqual(self, a1, a2) # self.assertCountEqual(a1, a2)
six.assertRaisesRegex(self, e, r, fn) # self.assertRaisesRegex(e, r, fn)
six.assertRegex(self, s, r) # self.assertRegex(s, r)
# Replace call on arg by arg attribute
six.get_method_function(meth) # meth.__func__
six.get_method_self(meth) # meth.__self__
six.get_function_closure(fn) # fn.__closure__
six.get_function_code(fn) # fn.__code__
six.get_function_defaults(fn) # fn.__defaults__
six.get_function_globals(fn) # fn.__globals__
# Replace by string literal
six.b("...") # b'...'
six.u("...") # '...'
six.ensure_binary("...") # b'...'
six.ensure_str("...") # '...'
six.ensure_text("...") # '...'
six.b(string) # no change
# Replace by simple expression
six.get_unbound_function(meth) # meth
six.create_unbound_method(fn, cls) # fn
# Raise exception
six.raise_from(exc, exc_from) # raise exc from exc_from
six.reraise(tp, exc, tb) # raise exc.with_traceback(tb)
six.reraise(*sys.exc_info()) # raise
# Int / Bytes conversion
six.byte2int(bs) # bs[0]
six.indexbytes(bs, i) # bs[i]
six.int2byte(i) # bytes((i, ))
# Special cases for next calls
next(six.iteritems(dct)) # next(iter(dct.items()))
next(six.iterkeys(dct)) # next(iter(dct.keys()))
next(six.itervalues(dct)) # next(iter(dct.values()))
# TODO: To implement
# Rewrite classes
@six.python_2_unicode_compatible # Remove
class C(six.Iterator):
pass # class C: pass
class C(six.with_metaclass(M, B)):
pass # class C(B, metaclass=M): pass
# class C(B, metaclass=M): pass
@six.add_metaclass(M)
class C(B):
pass

View File

@@ -0,0 +1,11 @@
import datetime
import datetime as dt
from datetime import timezone
from datetime import timezone as tz
print(datetime.timezone(-1))
print(timezone.utc)
print(tz.utc)
print(datetime.timezone.utc)
print(dt.timezone.utc)

View File

@@ -69,3 +69,20 @@ _ = """Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
""" # noqa
# Valid
# this is a veryyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy long comment # noqa: E501
# Valid
_ = """Here's a source: https://github.com/ethereum/web3.py/blob/ffe59daf10edc19ee5f05227b25bac8d090e8aa4/web3/_utils/events.py#L201
May raise:
- DeserializationError if the abi string is invalid or abi or log topics/data do not match
""" # noqa: E501
import collections # noqa
import os # noqa: F401, RUF100
import shelve # noqa: RUF100
import sys # noqa: F401, RUF100
print(sys.path)

View File

@@ -0,0 +1,2 @@
[tool.ruff]
src = ["."]

View File

@@ -0,0 +1,4 @@
from package.core import method
if __name__ == "__main__":
method()

1
resources/test/project/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
examples/generated

View File

@@ -0,0 +1,94 @@
# project
An example multi-package Python project used to test setting resolution and other complex
behaviors.
## Expected behavior
Running from the repo root should pick up and enforce the appropriate settings for each package:
```
∴ cargo run resources/test/project/
resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
resources/test/project/project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
Found 7 error(s).
6 potentially fixable with the --fix option.
```
Running from the project directory itself should exhibit the same behavior:
```
∴ (cd resources/test/project/ && cargo run .)
examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
project/file.py:1:8: F401 `os` imported but unused
project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
Found 7 error(s).
6 potentially fixable with the --fix option.
```
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
files:
```
∴ (cd resources/test/project/examples/docs && cargo run .)
docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
Found 2 error(s).
1 potentially fixable with the --fix option.
```
`--config` should force Ruff to use the specified `pyproject.toml` for all files, and resolve
file paths from the current working directory:
```
∴ (cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/)
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but unused
resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but unused
resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
Found 9 error(s).
9 potentially fixable with the --fix option.
```
Running from a parent directory should this "ignore" the `exclude` (hence, `concepts/file.py` gets
included in the output):
```
∴ (cd resources/test/project/examples && cargo run -- --config=docs/pyproject.toml .)
docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
excluded/script.py:5:5: F841 Local variable `x` is assigned to but never used
Found 4 error(s).
1 potentially fixable with the --fix option.
```
Passing an excluded directory directly should report errors in the contained files:
```
∴ cargo run resources/test/project/examples/excluded/
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
Found 1 error(s).
1 potentially fixable with the --fix option.
```
Unless we `--force-exclude`:
```
∴ cargo run resources/test/project/examples/excluded/ --force-exclude
∴ cargo run resources/test/project/examples/excluded/script.py --force-exclude
```

View File

@@ -0,0 +1,2 @@
import numpy as np
from app import app_file

View File

@@ -0,0 +1,5 @@
import os
def f():
x = 1

View File

@@ -0,0 +1,8 @@
import os
import numpy as np
from docs.concepts import file
def f():
x = 1

View File

@@ -0,0 +1,7 @@
[tool.ruff]
extend = "../../pyproject.toml"
src = ["."]
# Enable I001, and re-enable F841, to test extension priority.
extend-select = ["I001", "F841"]
extend-ignore = ["F401"]
extend-exclude = ["./docs/concepts/file.py"]

View File

@@ -0,0 +1,5 @@
import os
def f():
x = 1

View File

@@ -0,0 +1,5 @@
import os
def f():
x = 1

View File

@@ -0,0 +1,7 @@
import numpy as np
from app import app_file
from core import core_file
np.array([1, 2, 3])
app_file()
core_file()

View File

@@ -0,0 +1,5 @@
[tool.ruff]
src = [".", "python_modules/*"]
exclude = ["examples/excluded"]
extend-select = ["I001"]
extend-ignore = ["F841"]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.176"
version = "0.0.192"
edition = "2021"
[dependencies]
@@ -11,8 +11,8 @@ itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
once_cell = { version = "1.16.0" }
ruff = { path = ".." }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "c01f014b1269eedcf4bdb45d5fbc62ae2beecf31" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "c01f014b1269eedcf4bdb45d5fbc62ae2beecf31" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "c01f014b1269eedcf4bdb45d5fbc62ae2beecf31" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }

View File

@@ -1,18 +1,18 @@
//! Generate the `CheckCodePrefix` enum.
use std::collections::{BTreeMap, BTreeSet};
use std::fs::OpenOptions;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Output, Stdio};
use anyhow::Result;
use anyhow::{ensure, Result};
use clap::Parser;
use codegen::{Scope, Type, Variant};
use itertools::Itertools;
use ruff::checks::{CheckCode, CODE_REDIRECTS, PREFIX_REDIRECTS};
use ruff::checks::{CheckCode, PREFIX_REDIRECTS};
use strum::IntoEnumIterator;
const FILE: &str = "src/checks_gen.rs";
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
@@ -40,18 +40,7 @@ pub fn main(cli: &Cli) -> Result<()> {
}
// Add any prefix aliases (e.g., "U" to "UP").
for (alias, source) in PREFIX_REDIRECTS.iter() {
prefix_to_codes.insert(
(*alias).to_string(),
prefix_to_codes
.get(&(*source).to_string())
.unwrap_or_else(|| panic!("Unknown CheckCode: {source:?}"))
.clone(),
);
}
// Add any check code aliases (e.g., "U001" to "UP001").
for (alias, check_code) in CODE_REDIRECTS.iter() {
for (alias, check_code) in PREFIX_REDIRECTS.iter() {
prefix_to_codes.insert(
(*alias).to_string(),
prefix_to_codes
@@ -105,9 +94,9 @@ pub fn main(cli: &Cli) -> Result<()> {
.line("#[allow(clippy::match_same_arms)]")
.line("match self {");
for (prefix, codes) in &prefix_to_codes {
if let Some(target) = CODE_REDIRECTS.get(&prefix.as_str()) {
if let Some(target) = PREFIX_REDIRECTS.get(&prefix.as_str()) {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => {{ eprintln!(\"{{}}{{}} {{}}\", \
"CheckCodePrefix::{prefix} => {{ one_time_warning!(\"{{}}{{}} {{}}\", \
\"warning\".yellow().bold(), \":\".bold(), \"`{}` has been remapped to \
`{}`\".bold()); \n vec![{}] }}",
prefix,
@@ -117,18 +106,6 @@ pub fn main(cli: &Cli) -> Result<()> {
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
} else if let Some(target) = PREFIX_REDIRECTS.get(&prefix.as_str()) {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => {{ eprintln!(\"{{}}{{}} {{}}\", \
\"warning\".yellow().bold(), \":\".bold(), \"`{}` has been remapped to \
`{}`\".bold()); \n vec![{}] }}",
prefix,
target,
codes
.iter()
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
} else {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => vec![{}],",
@@ -182,6 +159,8 @@ pub fn main(cli: &Cli) -> Result<()> {
output.push('\n');
output.push_str("use crate::checks::CheckCode;");
output.push('\n');
output.push_str("use crate::one_time_warning;");
output.push('\n');
output.push('\n');
output.push_str(&scope.to_string());
output.push('\n');
@@ -202,12 +181,25 @@ pub fn main(cli: &Cli) -> Result<()> {
output.push('\n');
output.push('\n');
let rustfmt = Command::new("rustfmt")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
write!(rustfmt.stdin.as_ref().unwrap(), "{output}")?;
let Output { status, stdout, .. } = rustfmt.wait_with_output()?;
ensure!(status.success(), "rustfmt failed with {status}");
// Write the output to `src/checks_gen.rs` (or stdout).
if cli.dry_run {
println!("{output}");
println!("{}", String::from_utf8(stdout)?);
} else {
let mut f = OpenOptions::new().write(true).truncate(true).open(FILE)?;
write!(f, "{output}")?;
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("src/checks_gen.rs");
if fs::read(&file).map_or(true, |old| old != stdout) {
fs::write(&file, stdout)?;
}
}
Ok(())

View File

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

View File

@@ -1,13 +1,27 @@
use log::error;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{
Arguments, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, Stmt, StmtKind,
Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword, KeywordData,
Location, Stmt, StmtKind,
};
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
use crate::ast::types::Range;
use crate::SourceCodeLocator;
/// Create an `Expr` with default location from an `ExprKind`.
pub fn create_expr(node: ExprKind) -> Expr {
Expr::new(Location::default(), Location::default(), node)
}
/// Create a `Stmt` with a default location from a `StmtKind`.
pub fn create_stmt(node: StmtKind) -> Stmt {
Stmt::new(Location::default(), Location::default(), node)
}
fn collect_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
match &expr.node {
ExprKind::Call { func, .. } => {
@@ -149,15 +163,12 @@ pub fn match_call_path(
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
/// Return `true` if the `Stmt` is an assignment to a dunder (like `__all__`).
pub fn is_assignment_to_a_dunder(stmt: &Stmt) -> bool {
// Check whether it's an assignment to a dunder, with or without a type
// annotation. This is what pycodestyle (as of 2.9.1) does.
match node {
StmtKind::Assign {
targets,
value: _,
type_comment: _,
} => {
match &stmt.node {
StmtKind::Assign { targets, .. } => {
if targets.len() != 1 {
return false;
}
@@ -166,12 +177,7 @@ pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
_ => false,
}
}
StmtKind::AnnAssign {
target,
annotation: _,
value: _,
simple: _,
} => match &target.node {
StmtKind::AnnAssign { target, .. } => match &target.node {
ExprKind::Name { id, ctx: _ } => DUNDER_REGEX.is_match(id),
_ => false,
},
@@ -179,6 +185,60 @@ pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
}
}
/// Return `true` if the `Expr` is a singleton (`None`, `True`, `False`, or
/// `...`).
pub fn is_singleton(expr: &Expr) -> bool {
matches!(
expr.node,
ExprKind::Constant {
value: Constant::None | Constant::Bool(_) | Constant::Ellipsis,
..
}
)
}
/// Return `true` if the `Expr` is a constant or tuple of constants.
pub fn is_constant(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Constant { .. } => true,
ExprKind::Tuple { elts, .. } => elts.iter().all(is_constant),
_ => false,
}
}
/// Return `true` if the `Expr` is a non-singleton constant.
pub fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// Return the `Keyword` with the given name, if it's present in the list of
/// `Keyword` arguments.
pub fn find_keyword<'a>(keywords: &'a [Keyword], keyword_name: &str) -> Option<&'a Keyword> {
keywords.iter().find(|keyword| {
let KeywordData { arg, .. } = &keyword.node;
arg.as_ref().map_or(false, |arg| arg == keyword_name)
})
}
/// Return `true` if an `Expr` is `None`.
pub fn is_const_none(expr: &Expr) -> bool {
matches!(
&expr.node,
ExprKind::Constant {
value: Constant::None,
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| {
let KeywordData { value, .. } = &keyword.node;
!is_const_none(value)
})
}
/// Extract the names of all handled exceptions.
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<Vec<&str>> {
let mut handler_names = vec![];
@@ -229,7 +289,6 @@ pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> {
/// Returns `true` if a call is an argumented `super` invocation.
pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
// Check: is this a `super` call?
if let ExprKind::Name { id, .. } = &func.node {
id == "super" && !args.is_empty()
} else {
@@ -311,13 +370,75 @@ pub fn count_trailing_lines(stmt: &Stmt, locator: &SourceCodeLocator) -> usize {
.count()
}
/// Return the appropriate visual `Range` for any message that spans a `Stmt`.
/// Specifically, this method returns the range of a function or class name,
/// rather than that of the entire function or class body.
pub fn identifier_range(stmt: &Stmt, locator: &SourceCodeLocator) -> Range {
if matches!(
stmt.node,
StmtKind::ClassDef { .. }
| StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
) {
let contents = locator.slice_source_code_range(&Range::from_located(stmt));
for (start, tok, end) in lexer::make_tokenizer(&contents).flatten() {
if matches!(tok, Tok::Name { .. }) {
let start = to_absolute(start, stmt.location);
let end = to_absolute(end, stmt.location);
return Range {
location: start,
end_location: end,
};
}
}
error!("Failed to find identifier for {:?}", stmt);
}
Range::from_located(stmt)
}
/// Return `true` if a `Stmt` appears to be part of a multi-statement line, with
/// other statements preceding it.
pub fn preceded_by_continuation(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
// Does the previous line end in a continuation? This will have a specific
// false-positive, which is that if the previous line ends in a comment, it
// will be treated as a continuation. So we should only use this information to
// make conservative choices.
// TODO(charlie): Come up with a more robust strategy.
if stmt.location.row() > 1 {
let range = Range {
location: Location::new(stmt.location.row() - 1, 0),
end_location: Location::new(stmt.location.row(), 0),
};
let line = locator.slice_source_code_range(&range);
if line.trim().ends_with('\\') {
return true;
}
}
false
}
/// Return `true` if a `Stmt` appears to be part of a multi-statement line, with
/// other statements preceding it.
pub fn preceded_by_multi_statement_line(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
match_leading_content(stmt, locator) || preceded_by_continuation(stmt, locator)
}
/// Return `true` if a `Stmt` appears to be part of a multi-statement line, with
/// other statements following it.
pub fn followed_by_multi_statement_line(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
match_trailing_content(stmt, locator)
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Location;
use rustpython_parser::parser;
use crate::ast::helpers::match_module_member;
use crate::ast::helpers::{identifier_range, match_module_member, match_trailing_content};
use crate::ast::types::Range;
use crate::source_code_locator::SourceCodeLocator;
#[test]
fn builtin() -> Result<()> {
@@ -461,4 +582,130 @@ mod tests {
));
Ok(())
}
#[test]
fn trailing_content() -> Result<()> {
let contents = "x = 1";
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert!(!match_trailing_content(stmt, &locator));
let contents = "x = 1; y = 2";
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert!(match_trailing_content(stmt, &locator));
let contents = "x = 1 ";
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert!(!match_trailing_content(stmt, &locator));
let contents = "x = 1 # Comment";
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert!(!match_trailing_content(stmt, &locator));
let contents = r#"
x = 1
y = 2
"#
.trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert!(!match_trailing_content(stmt, &locator));
Ok(())
}
#[test]
fn extract_identifier_range() -> Result<()> {
let contents = "def f(): pass".trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 4),
end_location: Location::new(1, 5),
}
);
let contents = r#"
def \
f():
pass
"#
.trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(2, 2),
end_location: Location::new(2, 3),
}
);
let contents = "class Class(): pass".trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 6),
end_location: Location::new(1, 11),
}
);
let contents = "class Class: pass".trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 6),
end_location: Location::new(1, 11),
}
);
let contents = r#"
@decorator()
class Class():
pass
"#
.trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(2, 6),
end_location: Location::new(2, 11),
}
);
let contents = r#"x = y + 1"#.trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 0),
end_location: Location::new(1, 9),
}
);
Ok(())
}
}

View File

@@ -4,7 +4,7 @@ use std::str::Lines;
use rustpython_ast::{Located, Location};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checkers::ast::Checker;
/// Extract the leading indentation from a line.
pub fn indentation<'a, T>(checker: &'a Checker, located: &'a Located<T>) -> Cow<'a, str> {

View File

@@ -10,7 +10,7 @@ use crate::autofix::Fix;
use crate::checks::Check;
use crate::source_code_locator::SourceCodeLocator;
#[derive(Hash)]
#[derive(Debug, Copy, Clone, Hash)]
pub enum Mode {
Generate,
Apply,
@@ -27,15 +27,6 @@ impl From<bool> for Mode {
}
}
impl From<&Mode> for bool {
fn from(value: &Mode) -> Self {
match value {
Mode::Generate | Mode::Apply => true,
Mode::None => false,
}
}
}
/// Auto-fix errors in a file, and write the fixed source code to disk.
pub fn fix_file<'a>(
checks: &'a [Check],

View File

@@ -2,7 +2,11 @@ use anyhow::{bail, Result};
use itertools::Itertools;
use rustpython_parser::ast::{ExcepthandlerKind, Location, Stmt, StmtKind};
use crate::ast::helpers;
use crate::ast::helpers::to_absolute;
use crate::ast::whitespace::LinesWithTrailingNewline;
use crate::autofix::Fix;
use crate::source_code_locator::SourceCodeLocator;
/// Determine if a body contains only a single statement, taking into account
/// deleted.
@@ -66,7 +70,87 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
}
}
pub fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
/// Return the location of a trailing semicolon following a `Stmt`, if it's part
/// of a multi-statement line.
fn trailing_semicolon(stmt: &Stmt, locator: &SourceCodeLocator) -> Option<Location> {
let contents = locator.slice_source_code_at(&stmt.end_location.unwrap());
for (row, line) in LinesWithTrailingNewline::from(&contents).enumerate() {
let trimmed = line.trim();
if trimmed.starts_with(';') {
let column = line
.char_indices()
.find_map(|(column, char)| if char == ';' { Some(column) } else { None })
.unwrap();
return Some(to_absolute(
Location::new(row + 1, column),
stmt.end_location.unwrap(),
));
}
if !trimmed.starts_with('\\') {
break;
}
}
None
}
/// Find the next valid break for a `Stmt` after a semicolon.
fn next_stmt_break(semicolon: Location, locator: &SourceCodeLocator) -> Location {
let start_location = Location::new(semicolon.row(), semicolon.column() + 1);
let contents = locator.slice_source_code_at(&start_location);
for (row, line) in LinesWithTrailingNewline::from(&contents).enumerate() {
let trimmed = line.trim();
// Skip past any continuations.
if trimmed.starts_with('\\') {
continue;
}
return if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
to_absolute(Location::new(row + 1, 0), start_location)
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let column = line
.char_indices()
.find_map(|(column, char)| {
if char.is_whitespace() {
None
} else {
Some(column)
}
})
.unwrap();
to_absolute(Location::new(row + 1, column), start_location)
};
}
Location::new(start_location.row() + 1, 0)
}
/// Return `true` if a `Stmt` occurs at the end of a file.
fn is_end_of_file(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
let contents = locator.slice_source_code_at(&stmt.end_location.unwrap());
contents.is_empty()
}
/// Return the `Fix` to use when deleting a `Stmt`.
///
/// In some cases, this is as simple as deleting the `Range` of the `Stmt`
/// itself. However, there are a few exceptions:
/// - If the `Stmt` is _not_ the terminal statement in a multi-statement line,
/// we need to delete up to the start of the next statement (and avoid
/// deleting any content that precedes the statement).
/// - If the `Stmt` is the terminal statement in a multi-statement line, we need
/// to avoid deleting any content that precedes the statement.
/// - If the `Stmt` has no trailing and leading content, then it's convenient to
/// remove the entire start and end lines.
/// - If the `Stmt` is the last statement in its parent body, replace it with a
/// `pass` instead.
pub fn delete_stmt(
stmt: &Stmt,
parent: Option<&Stmt>,
deleted: &[&Stmt],
locator: &SourceCodeLocator,
) -> Result<Fix> {
if parent
.map(|parent| is_lone_child(stmt, parent, deleted))
.map_or(Ok(None), |v| v.map(Some))?
@@ -80,12 +164,103 @@ pub fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Res
stmt.end_location.unwrap(),
))
} else {
// Otherwise, nuke the entire line.
// TODO(charlie): This logic assumes that there are no multi-statement physical
// lines.
Ok(Fix::deletion(
Location::new(stmt.location.row(), 0),
Location::new(stmt.end_location.unwrap().row() + 1, 0),
))
Ok(if let Some(semicolon) = trailing_semicolon(stmt, locator) {
let next = next_stmt_break(semicolon, locator);
Fix::deletion(stmt.location, next)
} else if helpers::match_leading_content(stmt, locator) {
Fix::deletion(stmt.location, stmt.end_location.unwrap())
} else if helpers::preceded_by_continuation(stmt, locator) {
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())
} else {
Fix::deletion(stmt.location, stmt.end_location.unwrap())
}
} else {
Fix::deletion(
Location::new(stmt.location.row(), 0),
Location::new(stmt.end_location.unwrap().row() + 1, 0),
)
})
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use rustpython_ast::Location;
use rustpython_parser::parser;
use crate::autofix::helpers::{next_stmt_break, trailing_semicolon};
use crate::source_code_locator::SourceCodeLocator;
#[test]
fn find_semicolon() -> Result<()> {
let contents = "x = 1";
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(trailing_semicolon(stmt, &locator), None);
let contents = "x = 1; y = 1";
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
trailing_semicolon(stmt, &locator),
Some(Location::new(1, 5))
);
let contents = "x = 1 ; y = 1";
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
trailing_semicolon(stmt, &locator),
Some(Location::new(1, 6))
);
let contents = r#"
x = 1 \
; y = 1
"#
.trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
trailing_semicolon(stmt, &locator),
Some(Location::new(2, 2))
);
Ok(())
}
#[test]
fn find_next_stmt_break() {
let contents = "x = 1; y = 1";
let locator = SourceCodeLocator::new(contents);
assert_eq!(
next_stmt_break(Location::new(1, 4), &locator),
Location::new(1, 5)
);
let contents = "x = 1 ; y = 1";
let locator = SourceCodeLocator::new(contents);
assert_eq!(
next_stmt_break(Location::new(1, 5), &locator),
Location::new(1, 6)
);
let contents = r#"
x = 1 \
; y = 1
"#
.trim();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
next_stmt_break(Location::new(2, 2), &locator),
Location::new(2, 4)
);
}
}

View File

@@ -35,12 +35,4 @@ impl Fix {
end_location: at,
}
}
pub fn dummy(location: Location) -> Self {
Self {
content: String::new(),
location,
end_location: location,
}
}
}

View File

@@ -8,13 +8,15 @@ use std::path::Path;
use anyhow::Result;
use filetime::FileTime;
use log::error;
use once_cell::sync::Lazy;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
use crate::autofix::fixer;
use crate::message::Message;
use crate::settings::Settings;
use crate::settings::{flags, Settings};
static CACHE_DIR: Lazy<Option<String>> = Lazy::new(|| std::env::var("RUFF_CACHE_DIR").ok());
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Serialize, Deserialize)]
@@ -34,55 +36,18 @@ struct CheckResult {
messages: Vec<Message>,
}
pub enum Mode {
ReadWrite,
ReadOnly,
WriteOnly,
None,
fn cache_dir() -> &'static Path {
Path::new(CACHE_DIR.as_ref().map_or(".ruff_cache", String::as_str))
}
impl Mode {
fn allow_read(&self) -> bool {
match self {
Mode::ReadWrite => true,
Mode::ReadOnly => true,
Mode::WriteOnly => false,
Mode::None => false,
}
}
fn allow_write(&self) -> bool {
match self {
Mode::ReadWrite => true,
Mode::ReadOnly => false,
Mode::WriteOnly => true,
Mode::None => false,
}
}
fn content_dir() -> &'static Path {
Path::new("content")
}
impl From<bool> for Mode {
fn from(value: bool) -> Self {
if value {
Mode::ReadWrite
} else {
Mode::None
}
}
}
fn cache_dir() -> &'static str {
"./.ruff_cache"
}
fn content_dir() -> &'static str {
"content"
}
fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> u64 {
fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: fixer::Mode) -> u64 {
let mut hasher = DefaultHasher::new();
CARGO_PKG_VERSION.hash(&mut hasher);
path.absolutize().unwrap().hash(&mut hasher);
path.as_ref().absolutize().unwrap().hash(&mut hasher);
settings.hash(&mut hasher);
autofix.hash(&mut hasher);
hasher.finish()
@@ -90,7 +55,7 @@ fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> u64 {
/// Initialize the cache directory.
pub fn init() -> Result<()> {
let path = Path::new(cache_dir());
let path = cache_dir();
// Create the cache directories.
create_dir_all(path.join(content_dir()))?;
@@ -112,30 +77,24 @@ pub fn init() -> Result<()> {
fn write_sync(key: u64, value: &[u8]) -> Result<(), std::io::Error> {
fs::write(
Path::new(cache_dir())
.join(content_dir())
.join(format!("{key:x}")),
cache_dir().join(content_dir()).join(format!("{key:x}")),
value,
)
}
fn read_sync(key: u64) -> Result<Vec<u8>, std::io::Error> {
fs::read(
Path::new(cache_dir())
.join(content_dir())
.join(format!("{key:x}")),
)
fs::read(cache_dir().join(content_dir()).join(format!("{key:x}")))
}
/// Get a value from the cache.
pub fn get(
path: &Path,
pub fn get<P: AsRef<Path>>(
path: P,
metadata: &Metadata,
settings: &Settings,
autofix: &fixer::Mode,
mode: &Mode,
autofix: fixer::Mode,
cache: flags::Cache,
) -> Option<Vec<Message>> {
if !mode.allow_read() {
if matches!(cache, flags::Cache::Disabled) {
return None;
};
@@ -157,15 +116,15 @@ pub fn get(
}
/// Set a value in the cache.
pub fn set(
path: &Path,
pub fn set<P: AsRef<Path>>(
path: P,
metadata: &Metadata,
settings: &Settings,
autofix: &fixer::Mode,
autofix: fixer::Mode,
messages: &[Message],
mode: &Mode,
cache: flags::Cache,
) {
if !mode.allow_write() {
if matches!(cache, flags::Cache::Disabled) {
return;
};

View File

@@ -1,288 +0,0 @@
//! Lint rules based on checking raw physical lines.
use nohash_hasher::IntMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checks::{Check, CheckCode, CheckKind, CODE_REDIRECTS};
use crate::noqa;
use crate::noqa::{is_file_exempt, Directive};
use crate::settings::Settings;
// Regex from PEP263.
static CODING_COMMENT_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[ \t\f]*#.*?coding[:=][ \t]*utf-?8").unwrap());
static URL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^https?://\S+$").unwrap());
/// Whether the given line is too long and should be reported.
fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
if length <= limit {
return false;
}
let mut chunks = line.split_whitespace();
let (Some(first), Some(_)) = (chunks.next(), chunks.next()) else {
// Single word / no printable chars - no way to make the line shorter
return false;
};
// Do not enforce the line length for commented lines that end with a URL
// or contain only a single word.
!(first == "#" && chunks.last().map_or(true, |c| URL_REGEX.is_match(c)))
}
pub fn check_lines(
checks: &mut Vec<Check>,
contents: &str,
noqa_line_for: &IntMap<usize, usize>,
settings: &Settings,
autofix: bool,
ignore_noqa: bool,
) {
let enforce_unnecessary_coding_comment = settings.enabled.contains(&CheckCode::UP009);
let enforce_line_too_long = settings.enabled.contains(&CheckCode::E501);
let enforce_noqa = settings.enabled.contains(&CheckCode::RUF100);
let mut noqa_directives: IntMap<usize, (Directive, Vec<&str>)> = IntMap::default();
let mut line_checks = vec![];
let mut ignored = vec![];
checks.sort_by_key(|check| check.location);
let mut checks_iter = checks.iter().enumerate().peekable();
if let Some((_index, check)) = checks_iter.peek() {
assert!(check.location.row() >= 1);
}
macro_rules! add_if {
($check:expr, $noqa_lineno:expr, $line:expr) => {{
match noqa_directives
.entry($noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive($line), vec![]))
{
(Directive::All(..), matches) => {
matches.push($check.kind.code().as_ref());
if ignore_noqa {
line_checks.push($check);
}
}
(Directive::Codes(.., codes), matches) => {
if noqa::includes($check.kind.code(), codes) {
matches.push($check.kind.code().as_ref());
if ignore_noqa {
line_checks.push($check);
}
} else {
line_checks.push($check);
}
}
(Directive::None, ..) => line_checks.push($check),
}
}};
}
let lines: Vec<&str> = contents.lines().collect();
for (lineno, line) in lines.iter().enumerate() {
// If we hit an exemption for the entire file, bail.
if is_file_exempt(line) {
checks.drain(..);
return;
}
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
// Enforce unnecessary coding comments (UP009).
if enforce_unnecessary_coding_comment {
if lineno < 2 {
// PEP3120 makes utf-8 the default encoding.
if CODING_COMMENT_REGEX.is_match(line) {
let mut check = Check::new(
CheckKind::PEP3120UnnecessaryCodingComment,
Range {
location: Location::new(lineno + 1, 0),
end_location: Location::new(lineno + 2, 0),
},
);
if autofix && settings.fixable.contains(check.kind.code()) {
check.amend(Fix::deletion(
Location::new(lineno + 1, 0),
Location::new(lineno + 2, 0),
));
}
add_if!(check, noqa_lineno, lines[noqa_lineno]);
}
}
}
if enforce_noqa {
noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
}
// Remove any ignored checks.
while let Some((index, check)) =
checks_iter.next_if(|(_index, check)| check.location.row() == lineno + 1)
{
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
match noqa {
(Directive::All(..), matches) => {
matches.push(check.kind.code().as_ref());
ignored.push(index);
}
(Directive::Codes(.., codes), matches) => {
if noqa::includes(check.kind.code(), codes) {
matches.push(check.kind.code().as_ref());
ignored.push(index);
}
}
(Directive::None, ..) => {}
}
}
// Enforce line length violations (E501).
if enforce_line_too_long {
let line_length = line.chars().count();
if should_enforce_line_length(line, line_length, settings.line_length) {
let check = Check::new(
CheckKind::LineTooLong(line_length, settings.line_length),
Range {
location: Location::new(lineno + 1, 0),
end_location: Location::new(lineno + 1, line_length),
},
);
add_if!(check, noqa_lineno, lines[noqa_lineno]);
}
}
}
// Enforce newlines at end of files (W292).
if settings.enabled.contains(&CheckCode::W292) && !contents.ends_with('\n') {
// Note: if `lines.last()` is `None`, then `contents` is empty (and so we don't
// want to raise W292 anyway).
if let Some(line) = lines.last() {
let check = Check::new(
CheckKind::NoNewLineAtEndOfFile,
Range {
location: Location::new(lines.len(), line.len() + 1),
end_location: Location::new(lines.len(), line.len() + 1),
},
);
let lineno = lines.len() - 1;
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
add_if!(check, noqa_lineno, lines[noqa_lineno]);
}
}
// Enforce that the noqa directive was actually used (RUF100).
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
match directive {
Directive::All(spaces, start, end) => {
if matches.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(None),
Range {
location: Location::new(row + 1, start),
end_location: Location::new(row + 1, end),
},
);
if autofix && settings.fixable.contains(check.kind.code()) {
check.amend(Fix::deletion(
Location::new(row + 1, start - spaces),
Location::new(row + 1, lines[row].chars().count()),
));
}
line_checks.push(check);
}
}
Directive::Codes(spaces, start, end, codes) => {
let mut invalid_codes = vec![];
let mut valid_codes = vec![];
for code in codes {
let code = CODE_REDIRECTS.get(code).map_or(code, AsRef::as_ref);
if matches.contains(&code) || settings.external.contains(code) {
valid_codes.push(code.to_string());
} else {
invalid_codes.push(code.to_string());
}
}
if !invalid_codes.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(invalid_codes)),
Range {
location: Location::new(row + 1, start),
end_location: Location::new(row + 1, end),
},
);
if autofix && settings.fixable.contains(check.kind.code()) {
if valid_codes.is_empty() {
check.amend(Fix::deletion(
Location::new(row + 1, start - spaces),
Location::new(row + 1, lines[row].chars().count()),
));
} else {
check.amend(Fix::replacement(
format!("# noqa: {}", valid_codes.join(", ")),
Location::new(row + 1, start),
Location::new(row + 1, lines[row].chars().count()),
));
}
}
line_checks.push(check);
}
}
Directive::None => {}
}
}
}
if !ignore_noqa {
ignored.sort_unstable();
for index in ignored.iter().rev() {
checks.swap_remove(*index);
}
}
checks.extend(line_checks);
}
#[cfg(test)]
mod tests {
use nohash_hasher::IntMap;
use super::check_lines;
use crate::checks::{Check, CheckCode};
use crate::settings::Settings;
#[test]
fn e501_non_ascii_char() {
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
let check_with_max_line_length = |line_length: usize| {
let mut checks: Vec<Check> = vec![];
check_lines(
&mut checks,
line,
&IntMap::default(),
&Settings {
line_length,
..Settings::for_rule(CheckCode::E501)
},
true,
false,
);
checks
};
assert!(!check_with_max_line_length(6).is_empty());
assert!(check_with_max_line_length(7).is_empty());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,19 +9,22 @@ use crate::checks::Check;
use crate::directives::IsortDirectives;
use crate::isort;
use crate::isort::track::ImportTracker;
use crate::settings::Settings;
use crate::settings::{flags, Settings};
use crate::source_code_locator::SourceCodeLocator;
fn check_import_blocks(
tracker: ImportTracker,
locator: &SourceCodeLocator,
settings: &Settings,
autofix: bool,
autofix: flags::Autofix,
package: Option<&Path>,
) -> Vec<Check> {
let mut checks = vec![];
for block in tracker.into_iter() {
if !block.imports.is_empty() {
if let Some(check) = isort::plugins::check_imports(&block, locator, settings, autofix) {
if let Some(check) =
isort::plugins::check_imports(&block, locator, settings, autofix, package)
{
checks.push(check);
}
}
@@ -34,12 +37,13 @@ pub fn check_imports(
locator: &SourceCodeLocator,
directives: &IsortDirectives,
settings: &Settings,
autofix: bool,
autofix: flags::Autofix,
path: &Path,
package: Option<&Path>,
) -> Vec<Check> {
let mut tracker = ImportTracker::new(directives, path);
let mut tracker = ImportTracker::new(locator, directives, path);
for stmt in python_ast {
tracker.visit_stmt(stmt);
}
check_import_blocks(tracker, locator, settings, autofix)
check_import_blocks(tracker, locator, settings, autofix, package)
}

90
src/checkers/lines.rs Normal file
View File

@@ -0,0 +1,90 @@
//! Lint rules based on checking raw physical lines.
use crate::checks::{Check, CheckCode};
use crate::pycodestyle::checks::{line_too_long, no_newline_at_end_of_file};
use crate::pygrep_hooks::plugins::blanket_type_ignore;
use crate::pyupgrade::checks::unnecessary_coding_comment;
use crate::settings::{flags, Settings};
pub fn check_lines(
contents: &str,
commented_lines: &[usize],
settings: &Settings,
autofix: flags::Autofix,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
let enforce_unnecessary_coding_comment = settings.enabled.contains(&CheckCode::UP009);
let enforce_line_too_long = settings.enabled.contains(&CheckCode::E501);
let enforce_no_newline_at_end_of_file = settings.enabled.contains(&CheckCode::W292);
let enforce_blanket_type_ignore = settings.enabled.contains(&CheckCode::PGH003);
let mut commented_lines_iter = commented_lines.iter().peekable();
for (index, line) in contents.lines().enumerate() {
while commented_lines_iter
.next_if(|lineno| &(index + 1) == *lineno)
.is_some()
{
if enforce_unnecessary_coding_comment {
if index < 2 {
if let Some(check) = unnecessary_coding_comment(
index,
line,
matches!(autofix, flags::Autofix::Enabled)
&& settings.fixable.contains(&CheckCode::UP009),
) {
checks.push(check);
}
}
}
if enforce_blanket_type_ignore {
if commented_lines.contains(&(index + 1)) {
if let Some(check) = blanket_type_ignore(index, line) {
checks.push(check);
}
}
}
}
if enforce_line_too_long {
if let Some(check) = line_too_long(index, line, settings.line_length) {
checks.push(check);
}
}
}
if enforce_no_newline_at_end_of_file {
if let Some(check) = no_newline_at_end_of_file(contents) {
checks.push(check);
}
}
checks
}
#[cfg(test)]
mod tests {
use super::check_lines;
use crate::checks::CheckCode;
use crate::settings::{flags, Settings};
#[test]
fn e501_non_ascii_char() {
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
let check_with_max_line_length = |line_length: usize| {
check_lines(
line,
&[],
&Settings {
line_length,
..Settings::for_rule(CheckCode::E501)
},
flags::Autofix::Enabled,
)
};
assert!(!check_with_max_line_length(6).is_empty());
assert!(check_with_max_line_length(7).is_empty());
}
}

5
src/checkers/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod ast;
pub mod imports;
pub mod lines;
pub mod noqa;
pub mod tokens;

162
src/checkers/noqa.rs Normal file
View File

@@ -0,0 +1,162 @@
//! `NoQA` enforcement and validation.
use nohash_hasher::IntMap;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checks::{Check, CheckCode, CheckKind, CODE_REDIRECTS};
use crate::noqa;
use crate::noqa::{is_file_exempt, Directive};
use crate::settings::{flags, Settings};
pub fn check_noqa(
checks: &mut Vec<Check>,
contents: &str,
commented_lines: &[usize],
noqa_line_for: &IntMap<usize, usize>,
settings: &Settings,
autofix: flags::Autofix,
) {
let mut noqa_directives: IntMap<usize, (Directive, Vec<&str>)> = IntMap::default();
let mut ignored = vec![];
let enforce_noqa = settings.enabled.contains(&CheckCode::RUF100);
checks.sort_by_key(|check| check.location);
let mut checks_iter = checks.iter().enumerate().peekable();
if let Some((_index, check)) = checks_iter.peek() {
assert!(check.location.row() >= 1);
}
let lines: Vec<&str> = contents.lines().collect();
for lineno in commented_lines {
// If we hit an exemption for the entire file, bail.
if is_file_exempt(lines[lineno - 1]) {
checks.drain(..);
return;
}
if enforce_noqa {
noqa_directives
.entry(lineno - 1)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[lineno - 1]), vec![]));
}
// Remove any ignored checks.
while let Some((index, check)) =
checks_iter.next_if(|(_index, check)| check.location.row() <= *lineno)
{
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let check_lineno = check.location.row();
let noqa_lineno = noqa_line_for.get(&check_lineno).unwrap_or(&check_lineno);
if noqa_lineno == lineno {
let noqa = noqa_directives.entry(noqa_lineno - 1).or_insert_with(|| {
(noqa::extract_noqa_directive(lines[noqa_lineno - 1]), vec![])
});
match noqa {
(Directive::All(..), matches) => {
matches.push(check.kind.code().as_ref());
ignored.push(index);
}
(Directive::Codes(.., codes), matches) => {
if noqa::includes(check.kind.code(), codes) {
matches.push(check.kind.code().as_ref());
ignored.push(index);
}
}
(Directive::None, ..) => {}
}
}
}
}
// Enforce that the noqa directive was actually used (RUF100).
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
match directive {
Directive::All(spaces, start, end) => {
if matches.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(None),
Range {
location: Location::new(row + 1, start),
end_location: Location::new(row + 1, end),
},
);
if matches!(autofix, flags::Autofix::Enabled)
&& settings.fixable.contains(check.kind.code())
{
check.amend(Fix::deletion(
Location::new(row + 1, start - spaces),
Location::new(row + 1, lines[row].chars().count()),
));
}
checks.push(check);
}
}
Directive::Codes(spaces, start, end, codes) => {
let mut invalid_codes = vec![];
let mut valid_codes = vec![];
let mut self_ignore = false;
for code in codes {
let code = CODE_REDIRECTS.get(code).map_or(code, AsRef::as_ref);
if code == CheckCode::RUF100.as_ref() {
self_ignore = true;
} else {
if matches.contains(&code) || settings.external.contains(code) {
valid_codes.push(code);
} else {
invalid_codes.push(code);
}
}
}
if self_ignore {
continue;
}
if !invalid_codes.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(
invalid_codes
.iter()
.map(|code| (*code).to_string())
.collect(),
)),
Range {
location: Location::new(row + 1, start),
end_location: Location::new(row + 1, end),
},
);
if matches!(autofix, flags::Autofix::Enabled)
&& settings.fixable.contains(check.kind.code())
{
if valid_codes.is_empty() {
check.amend(Fix::deletion(
Location::new(row + 1, start - spaces),
Location::new(row + 1, lines[row].chars().count()),
));
} else {
check.amend(Fix::replacement(
format!("# noqa: {}", valid_codes.join(", ")),
Location::new(row + 1, start),
Location::new(row + 1, lines[row].chars().count()),
));
}
}
checks.push(check);
}
}
Directive::None => {}
}
}
}
ignored.sort_unstable();
for index in ignored.iter().rev() {
checks.swap_remove(*index);
}
}

View File

@@ -5,6 +5,7 @@ use rustpython_parser::lexer::{LexResult, Tok};
use crate::checks::{Check, CheckCode};
use crate::lex::docstring_detection::StateMachine;
use crate::ruff::checks::Context;
use crate::settings::flags;
use crate::source_code_locator::SourceCodeLocator;
use crate::{eradicate, flake8_quotes, pycodestyle, ruff, Settings};
@@ -12,7 +13,7 @@ pub fn check_tokens(
locator: &SourceCodeLocator,
tokens: &[LexResult],
settings: &Settings,
autofix: bool,
autofix: flags::Autofix,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];

View File

@@ -33,6 +33,7 @@ use crate::pyupgrade::types::Primitive;
)]
pub enum CheckCode {
// pycodestyle errors
E401,
E402,
E501,
E711,
@@ -223,6 +224,8 @@ pub enum CheckCode {
UP013,
UP014,
UP015,
UP016,
UP017,
// pydocstyle
D100,
D101,
@@ -308,6 +311,16 @@ pub enum CheckCode {
ARG005,
// flake8-import-conventions
ICN001,
// flake8-datetimez
DTZ001,
DTZ002,
DTZ003,
DTZ004,
DTZ005,
DTZ006,
DTZ007,
DTZ011,
DTZ012,
// Ruff
RUF001,
RUF002,
@@ -315,6 +328,25 @@ pub enum CheckCode {
RUF100,
// pygrep-hooks
PGH001,
PGH002,
PGH003,
// pandas-vet
PD002,
PD003,
PD004,
PD007,
PD008,
PD009,
PD010,
PD011,
PD012,
PD013,
PD015,
PD901,
// flake8-errmsg
EM101,
EM102,
EM103,
}
#[derive(EnumIter, Debug, PartialEq, Eq)]
@@ -335,6 +367,7 @@ pub enum CheckCategory {
Flake8Builtins,
Flake8Comprehensions,
Flake8Debugger,
Flake8ErrMsg,
Flake8ImportConventions,
Flake8Print,
Flake8Quotes,
@@ -342,7 +375,9 @@ pub enum CheckCategory {
Flake8Simplify,
Flake8TidyImports,
Flake8UnusedArguments,
Flake8Datetimez,
Eradicate,
PandasVet,
PygrepHooks,
Pylint,
Ruff,
@@ -375,6 +410,7 @@ impl CheckCategory {
CheckCategory::Flake8Builtins => "flake8-builtins",
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
CheckCategory::Flake8Debugger => "flake8-debugger",
CheckCategory::Flake8ErrMsg => "flake8-errmsg",
CheckCategory::Flake8ImportConventions => "flake8-import-conventions",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
@@ -382,8 +418,10 @@ impl CheckCategory {
CheckCategory::Flake8TidyImports => "flake8-tidy-imports",
CheckCategory::Flake8Simplify => "flake8-simplify",
CheckCategory::Flake8UnusedArguments => "flake8-unused-arguments",
CheckCategory::Flake8Datetimez => "flake8-datetimez",
CheckCategory::Isort => "isort",
CheckCategory::McCabe => "mccabe",
CheckCategory::PandasVet => "pandas-vet",
CheckCategory::PEP8Naming => "pep8-naming",
CheckCategory::Pycodestyle => "pycodestyle",
CheckCategory::Pydocstyle => "pydocstyle",
@@ -407,14 +445,17 @@ impl CheckCategory {
CheckCategory::Flake8Builtins => vec![CheckCodePrefix::A],
CheckCategory::Flake8Comprehensions => vec![CheckCodePrefix::C4],
CheckCategory::Flake8Debugger => vec![CheckCodePrefix::T10],
CheckCategory::Flake8ErrMsg => vec![CheckCodePrefix::EM],
CheckCategory::Flake8Print => vec![CheckCodePrefix::T20],
CheckCategory::Flake8Quotes => vec![CheckCodePrefix::Q],
CheckCategory::Flake8Return => vec![CheckCodePrefix::RET],
CheckCategory::Flake8Simplify => vec![CheckCodePrefix::SIM],
CheckCategory::Flake8TidyImports => vec![CheckCodePrefix::TID],
CheckCategory::Flake8UnusedArguments => vec![CheckCodePrefix::ARG],
CheckCategory::Flake8Datetimez => vec![CheckCodePrefix::DTZ],
CheckCategory::Isort => vec![CheckCodePrefix::I],
CheckCategory::McCabe => vec![CheckCodePrefix::C90],
CheckCategory::PandasVet => vec![CheckCodePrefix::PD],
CheckCategory::PEP8Naming => vec![CheckCodePrefix::N],
CheckCategory::Pycodestyle => vec![CheckCodePrefix::E, CheckCodePrefix::W],
CheckCategory::Pydocstyle => vec![CheckCodePrefix::D],
@@ -473,6 +514,10 @@ impl CheckCategory {
"https://pypi.org/project/flake8-debugger/4.1.2/",
&Platform::PyPI,
)),
CheckCategory::Flake8ErrMsg => Some((
"https://pypi.org/project/flake8-errmsg/0.4.0/",
&Platform::PyPI,
)),
CheckCategory::Flake8ImportConventions => None,
CheckCategory::Flake8Print => Some((
"https://pypi.org/project/flake8-print/5.0.0/",
@@ -498,12 +543,20 @@ impl CheckCategory {
"https://pypi.org/project/flake8-unused-arguments/0.0.12/",
&Platform::PyPI,
)),
CheckCategory::Flake8Datetimez => Some((
"https://pypi.org/project/flake8-datetimez/20.10.0/",
&Platform::PyPI,
)),
CheckCategory::Isort => {
Some(("https://pypi.org/project/isort/5.10.1/", &Platform::PyPI))
}
CheckCategory::McCabe => {
Some(("https://pypi.org/project/mccabe/0.7.0/", &Platform::PyPI))
}
CheckCategory::PandasVet => Some((
"https://pypi.org/project/pandas-vet/0.2.3/",
&Platform::PyPI,
)),
CheckCategory::PEP8Naming => Some((
"https://pypi.org/project/pep8-naming/0.13.2/",
&Platform::PyPI,
@@ -541,6 +594,7 @@ pub enum LintSource {
Lines,
Tokens,
Imports,
NoQA,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -592,6 +646,7 @@ pub enum CheckKind {
IOError(String),
LineTooLong(usize, usize),
ModuleImportNotAtTopOfFile,
MultipleImportsOnOneLine,
NoneComparison(RejectedCmpop),
NotInTest,
NotIsTest,
@@ -740,7 +795,7 @@ pub enum CheckKind {
MissingTypeCls(String),
MissingReturnTypePublicFunction(String),
MissingReturnTypePrivateFunction(String),
MissingReturnTypeMagicMethod(String),
MissingReturnTypeSpecialMethod(String),
MissingReturnTypeStaticMethod(String),
MissingReturnTypeClassMethod(String),
DynamicallyTypedExpression(String),
@@ -772,6 +827,8 @@ pub enum CheckKind {
ConvertTypedDictFunctionalToClass(String),
ConvertNamedTupleFunctionalToClass(String),
RedundantOpenModes,
RemoveSixCompat,
DatetimeTimezoneUTC,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -853,6 +910,8 @@ pub enum CheckKind {
BooleanPositionalValueInFunctionCall,
// pygrep-hooks
NoEval,
DeprecatedLogWarn,
BlanketTypeIgnore,
// flake8-unused-arguments
UnusedFunctionArgument(String),
UnusedMethodArgument(String),
@@ -861,11 +920,38 @@ pub enum CheckKind {
UnusedLambdaArgument(String),
// flake8-import-conventions
ImportAliasIsNotConventional(String, String),
// pandas-vet
UseOfInplaceArgument,
UseOfDotIsNull,
UseOfDotNotNull,
UseOfDotIx,
UseOfDotAt,
UseOfDotIat,
UseOfDotPivotOrUnstack,
UseOfDotValues,
UseOfDotReadTable,
UseOfDotStack,
UseOfPdMerge,
DfIsABadVariableName,
// flake8-errmsg
RawStringInException,
FStringInException,
DotFormatInException,
// Ruff
AmbiguousUnicodeCharacterString(char, char),
AmbiguousUnicodeCharacterDocstring(char, char),
AmbiguousUnicodeCharacterComment(char, char),
UnusedNOQA(Option<Vec<String>>),
// flake8-datetimez
CallDatetimeWithoutTzinfo,
CallDatetimeToday,
CallDatetimeUtcnow,
CallDatetimeUtcfromtimestamp,
CallDatetimeNowWithoutTzinfo,
CallDatetimeFromtimestamp,
CallDatetimeStrptimeWithoutZone,
CallDateToday,
CallDateFromtimestamp,
}
impl CheckCode {
@@ -873,7 +959,8 @@ impl CheckCode {
/// physical lines).
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E501 | CheckCode::W292 | CheckCode::RUF100 | CheckCode::UP009 => {
CheckCode::RUF100 => &LintSource::NoQA,
CheckCode::E501 | CheckCode::W292 | CheckCode::UP009 | CheckCode::PGH003 => {
&LintSource::Lines
}
CheckCode::ERA001
@@ -895,6 +982,7 @@ impl CheckCode {
pub fn kind(&self) -> CheckKind {
match self {
// pycodestyle errors
CheckCode::E401 => CheckKind::MultipleImportsOnOneLine,
CheckCode::E402 => CheckKind::ModuleImportNotAtTopOfFile,
CheckCode::E501 => CheckKind::LineTooLong(89, 88),
CheckCode::E711 => CheckKind::NoneComparison(RejectedCmpop::Eq),
@@ -1076,7 +1164,7 @@ impl CheckCode {
CheckCode::ANN102 => CheckKind::MissingTypeCls("...".to_string()),
CheckCode::ANN201 => CheckKind::MissingReturnTypePublicFunction("...".to_string()),
CheckCode::ANN202 => CheckKind::MissingReturnTypePrivateFunction("...".to_string()),
CheckCode::ANN204 => CheckKind::MissingReturnTypeMagicMethod("...".to_string()),
CheckCode::ANN204 => CheckKind::MissingReturnTypeSpecialMethod("...".to_string()),
CheckCode::ANN205 => CheckKind::MissingReturnTypeStaticMethod("...".to_string()),
CheckCode::ANN206 => CheckKind::MissingReturnTypeClassMethod("...".to_string()),
CheckCode::ANN401 => CheckKind::DynamicallyTypedExpression("...".to_string()),
@@ -1113,6 +1201,8 @@ impl CheckCode {
CheckCode::UP013 => CheckKind::ConvertTypedDictFunctionalToClass("...".to_string()),
CheckCode::UP014 => CheckKind::ConvertNamedTupleFunctionalToClass("...".to_string()),
CheckCode::UP015 => CheckKind::RedundantOpenModes,
CheckCode::UP016 => CheckKind::RemoveSixCompat,
CheckCode::UP017 => CheckKind::DatetimeTimezoneUTC,
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -1209,6 +1299,8 @@ impl CheckCode {
CheckCode::FBT003 => CheckKind::BooleanPositionalValueInFunctionCall,
// pygrep-hooks
CheckCode::PGH001 => CheckKind::NoEval,
CheckCode::PGH002 => CheckKind::DeprecatedLogWarn,
CheckCode::PGH003 => CheckKind::BlanketTypeIgnore,
// flake8-unused-arguments
CheckCode::ARG001 => CheckKind::UnusedFunctionArgument("...".to_string()),
CheckCode::ARG002 => CheckKind::UnusedMethodArgument("...".to_string()),
@@ -1219,6 +1311,33 @@ impl CheckCode {
CheckCode::ICN001 => {
CheckKind::ImportAliasIsNotConventional("...".to_string(), "...".to_string())
}
// pandas-vet
CheckCode::PD002 => CheckKind::UseOfInplaceArgument,
CheckCode::PD003 => CheckKind::UseOfDotIsNull,
CheckCode::PD004 => CheckKind::UseOfDotNotNull,
CheckCode::PD007 => CheckKind::UseOfDotIx,
CheckCode::PD008 => CheckKind::UseOfDotAt,
CheckCode::PD009 => CheckKind::UseOfDotIat,
CheckCode::PD010 => CheckKind::UseOfDotPivotOrUnstack,
CheckCode::PD011 => CheckKind::UseOfDotValues,
CheckCode::PD012 => CheckKind::UseOfDotReadTable,
CheckCode::PD013 => CheckKind::UseOfDotStack,
CheckCode::PD015 => CheckKind::UseOfPdMerge,
CheckCode::PD901 => CheckKind::DfIsABadVariableName,
// flake8-errmsg
CheckCode::EM101 => CheckKind::RawStringInException,
CheckCode::EM102 => CheckKind::FStringInException,
CheckCode::EM103 => CheckKind::DotFormatInException,
// flake8-datetimez
CheckCode::DTZ001 => CheckKind::CallDatetimeWithoutTzinfo,
CheckCode::DTZ002 => CheckKind::CallDatetimeToday,
CheckCode::DTZ003 => CheckKind::CallDatetimeUtcnow,
CheckCode::DTZ004 => CheckKind::CallDatetimeUtcfromtimestamp,
CheckCode::DTZ005 => CheckKind::CallDatetimeNowWithoutTzinfo,
CheckCode::DTZ006 => CheckKind::CallDatetimeFromtimestamp,
CheckCode::DTZ007 => CheckKind::CallDatetimeStrptimeWithoutZone,
CheckCode::DTZ011 => CheckKind::CallDateToday,
CheckCode::DTZ012 => CheckKind::CallDateFromtimestamp,
// Ruff
CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'),
CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'),
@@ -1340,6 +1459,16 @@ impl CheckCode {
CheckCode::D417 => CheckCategory::Pydocstyle,
CheckCode::D418 => CheckCategory::Pydocstyle,
CheckCode::D419 => CheckCategory::Pydocstyle,
CheckCode::DTZ001 => CheckCategory::Flake8Datetimez,
CheckCode::DTZ002 => CheckCategory::Flake8Datetimez,
CheckCode::DTZ003 => CheckCategory::Flake8Datetimez,
CheckCode::DTZ004 => CheckCategory::Flake8Datetimez,
CheckCode::DTZ005 => CheckCategory::Flake8Datetimez,
CheckCode::DTZ006 => CheckCategory::Flake8Datetimez,
CheckCode::DTZ007 => CheckCategory::Flake8Datetimez,
CheckCode::DTZ011 => CheckCategory::Flake8Datetimez,
CheckCode::DTZ012 => CheckCategory::Flake8Datetimez,
CheckCode::E401 => CheckCategory::Pycodestyle,
CheckCode::E402 => CheckCategory::Pycodestyle,
CheckCode::E501 => CheckCategory::Pycodestyle,
CheckCode::E711 => CheckCategory::Pycodestyle,
@@ -1354,6 +1483,9 @@ impl CheckCode {
CheckCode::E743 => CheckCategory::Pycodestyle,
CheckCode::E902 => CheckCategory::Pycodestyle,
CheckCode::E999 => CheckCategory::Pycodestyle,
CheckCode::EM101 => CheckCategory::Flake8ErrMsg,
CheckCode::EM102 => CheckCategory::Flake8ErrMsg,
CheckCode::EM103 => CheckCategory::Flake8ErrMsg,
CheckCode::ERA001 => CheckCategory::Eradicate,
CheckCode::F401 => CheckCategory::Pyflakes,
CheckCode::F402 => CheckCategory::Pyflakes,
@@ -1420,7 +1552,21 @@ impl CheckCode {
CheckCode::N816 => CheckCategory::PEP8Naming,
CheckCode::N817 => CheckCategory::PEP8Naming,
CheckCode::N818 => CheckCategory::PEP8Naming,
CheckCode::PD002 => CheckCategory::PandasVet,
CheckCode::PD003 => CheckCategory::PandasVet,
CheckCode::PD004 => CheckCategory::PandasVet,
CheckCode::PD007 => CheckCategory::PandasVet,
CheckCode::PD008 => CheckCategory::PandasVet,
CheckCode::PD009 => CheckCategory::PandasVet,
CheckCode::PD010 => CheckCategory::PandasVet,
CheckCode::PD011 => CheckCategory::PandasVet,
CheckCode::PD012 => CheckCategory::PandasVet,
CheckCode::PD013 => CheckCategory::PandasVet,
CheckCode::PD015 => CheckCategory::PandasVet,
CheckCode::PD901 => CheckCategory::PandasVet,
CheckCode::PGH001 => CheckCategory::PygrepHooks,
CheckCode::PGH002 => CheckCategory::PygrepHooks,
CheckCode::PGH003 => CheckCategory::PygrepHooks,
CheckCode::PLC0414 => CheckCategory::Pylint,
CheckCode::PLC2201 => CheckCategory::Pylint,
CheckCode::PLC3002 => CheckCategory::Pylint,
@@ -1473,6 +1619,8 @@ impl CheckCode {
CheckCode::UP013 => CheckCategory::Pyupgrade,
CheckCode::UP014 => CheckCategory::Pyupgrade,
CheckCode::UP015 => CheckCategory::Pyupgrade,
CheckCode::UP016 => CheckCategory::Pyupgrade,
CheckCode::UP017 => CheckCategory::Pyupgrade,
CheckCode::W292 => CheckCategory::Pycodestyle,
CheckCode::W605 => CheckCategory::Pycodestyle,
CheckCode::YTT101 => CheckCategory::Flake82020,
@@ -1517,6 +1665,7 @@ impl CheckKind {
CheckKind::IsLiteral => &CheckCode::F632,
CheckKind::LateFutureImport => &CheckCode::F404,
CheckKind::LineTooLong(..) => &CheckCode::E501,
CheckKind::MultipleImportsOnOneLine => &CheckCode::E401,
CheckKind::ModuleImportNotAtTopOfFile => &CheckCode::E402,
CheckKind::MultiValueRepeatedKeyLiteral => &CheckCode::F601,
CheckKind::MultiValueRepeatedKeyVariable(_) => &CheckCode::F602,
@@ -1649,7 +1798,7 @@ impl CheckKind {
CheckKind::MissingTypeCls(_) => &CheckCode::ANN102,
CheckKind::MissingReturnTypePublicFunction(_) => &CheckCode::ANN201,
CheckKind::MissingReturnTypePrivateFunction(_) => &CheckCode::ANN202,
CheckKind::MissingReturnTypeMagicMethod(_) => &CheckCode::ANN204,
CheckKind::MissingReturnTypeSpecialMethod(_) => &CheckCode::ANN204,
CheckKind::MissingReturnTypeStaticMethod(_) => &CheckCode::ANN205,
CheckKind::MissingReturnTypeClassMethod(_) => &CheckCode::ANN206,
CheckKind::DynamicallyTypedExpression(_) => &CheckCode::ANN401,
@@ -1681,6 +1830,8 @@ impl CheckKind {
CheckKind::ConvertTypedDictFunctionalToClass(_) => &CheckCode::UP013,
CheckKind::ConvertNamedTupleFunctionalToClass(_) => &CheckCode::UP014,
CheckKind::RedundantOpenModes => &CheckCode::UP015,
CheckKind::RemoveSixCompat => &CheckCode::UP016,
CheckKind::DatetimeTimezoneUTC => &CheckCode::UP017,
// pydocstyle
CheckKind::BlankLineAfterLastSection(_) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(_) => &CheckCode::D410,
@@ -1762,6 +1913,8 @@ impl CheckKind {
CheckKind::BooleanPositionalValueInFunctionCall => &CheckCode::FBT003,
// pygrep-hooks
CheckKind::NoEval => &CheckCode::PGH001,
CheckKind::DeprecatedLogWarn => &CheckCode::PGH002,
CheckKind::BlanketTypeIgnore => &CheckCode::PGH003,
// flake8-unused-arguments
CheckKind::UnusedFunctionArgument(..) => &CheckCode::ARG001,
CheckKind::UnusedMethodArgument(..) => &CheckCode::ARG002,
@@ -1770,6 +1923,33 @@ impl CheckKind {
CheckKind::UnusedLambdaArgument(..) => &CheckCode::ARG005,
// flake8-import-conventions
CheckKind::ImportAliasIsNotConventional(..) => &CheckCode::ICN001,
// pandas-vet
CheckKind::UseOfInplaceArgument => &CheckCode::PD002,
CheckKind::UseOfDotIsNull => &CheckCode::PD003,
CheckKind::UseOfDotNotNull => &CheckCode::PD004,
CheckKind::UseOfDotIx => &CheckCode::PD007,
CheckKind::UseOfDotAt => &CheckCode::PD008,
CheckKind::UseOfDotIat => &CheckCode::PD009,
CheckKind::UseOfDotPivotOrUnstack => &CheckCode::PD010,
CheckKind::UseOfDotValues => &CheckCode::PD011,
CheckKind::UseOfDotReadTable => &CheckCode::PD012,
CheckKind::UseOfDotStack => &CheckCode::PD013,
CheckKind::UseOfPdMerge => &CheckCode::PD015,
CheckKind::DfIsABadVariableName => &CheckCode::PD901,
// flake8-errmsg
CheckKind::RawStringInException => &CheckCode::EM101,
CheckKind::FStringInException => &CheckCode::EM102,
CheckKind::DotFormatInException => &CheckCode::EM103,
// flake8-datetimez
CheckKind::CallDatetimeWithoutTzinfo => &CheckCode::DTZ001,
CheckKind::CallDatetimeToday => &CheckCode::DTZ002,
CheckKind::CallDatetimeUtcnow => &CheckCode::DTZ003,
CheckKind::CallDatetimeUtcfromtimestamp => &CheckCode::DTZ004,
CheckKind::CallDatetimeNowWithoutTzinfo => &CheckCode::DTZ005,
CheckKind::CallDatetimeFromtimestamp => &CheckCode::DTZ006,
CheckKind::CallDatetimeStrptimeWithoutZone => &CheckCode::DTZ007,
CheckKind::CallDateToday => &CheckCode::DTZ011,
CheckKind::CallDateFromtimestamp => &CheckCode::DTZ012,
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001,
CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002,
@@ -1846,6 +2026,7 @@ impl CheckKind {
CheckKind::ModuleImportNotAtTopOfFile => {
"Module level import not at top of file".to_string()
}
CheckKind::MultipleImportsOnOneLine => "Multiple imports on one line".to_string(),
CheckKind::MultiValueRepeatedKeyLiteral => {
"Dictionary key literal repeated".to_string()
}
@@ -2288,8 +2469,8 @@ impl CheckKind {
CheckKind::MissingReturnTypePrivateFunction(name) => {
format!("Missing return type annotation for private function `{name}`")
}
CheckKind::MissingReturnTypeMagicMethod(name) => {
format!("Missing return type annotation for magic method `{name}`")
CheckKind::MissingReturnTypeSpecialMethod(name) => {
format!("Missing return type annotation for special method `{name}`")
}
CheckKind::MissingReturnTypeStaticMethod(name) => {
format!("Missing return type annotation for staticmethod `{name}`")
@@ -2334,7 +2515,7 @@ impl CheckKind {
}
// flake8-simplify
CheckKind::KeyInDict(key, dict) => {
format!("Use '{key} in {dict}' instead of '{key} in {dict}.keys()")
format!("Use `{key} in {dict}` instead of `{key} in {dict}.keys()`")
}
// pyupgrade
CheckKind::TypeOfPrimitive(primitive) => {
@@ -2372,6 +2553,8 @@ impl CheckKind {
}
CheckKind::UnnecessaryEncodeUTF8 => "Unnecessary call to `encode` as UTF-8".to_string(),
CheckKind::RedundantOpenModes => "Unnecessary open mode parameters".to_string(),
CheckKind::RemoveSixCompat => "Unnecessary `six` compatibility usage".to_string(),
CheckKind::DatetimeTimezoneUTC => "Use `datetime.UTC` alias".to_string(),
CheckKind::ConvertTypedDictFunctionalToClass(name) => {
format!("Convert `{name}` from `TypedDict` functional to class syntax")
}
@@ -2581,6 +2764,12 @@ impl CheckKind {
}
// pygrep-hooks
CheckKind::NoEval => "No builtin `eval()` allowed".to_string(),
CheckKind::DeprecatedLogWarn => {
"`warn` is deprecated in favor of `warning`".to_string()
}
CheckKind::BlanketTypeIgnore => {
"Use specific error codes when ignoring type issues".to_string()
}
// flake8-unused-arguments
CheckKind::UnusedFunctionArgument(name) => {
format!("Unused function argument: `{name}`")
@@ -2597,6 +2786,51 @@ impl CheckKind {
CheckKind::ImportAliasIsNotConventional(name, asname) => {
format!("`{name}` should be imported as `{asname}`")
}
// pandas-vet
CheckKind::UseOfInplaceArgument => {
"`inplace=True` should be avoided; it has inconsistent behavior".to_string()
}
CheckKind::UseOfDotIsNull => {
"`.isna` is preferred to `.isnull`; functionality is equivalent".to_string()
}
CheckKind::UseOfDotNotNull => {
"`.notna` is preferred to `.notnull`; functionality is equivalent".to_string()
}
CheckKind::UseOfDotIx => {
"`.ix` is deprecated; use more explicit `.loc` or `.iloc`".to_string()
}
CheckKind::UseOfDotAt => {
"Use `.loc` instead of `.at`. If speed is important, use numpy.".to_string()
}
CheckKind::UseOfDotIat => {
"Use `.iloc` instead of `.iat`. If speed is important, use numpy.".to_string()
}
CheckKind::UseOfDotPivotOrUnstack => "`.pivot_table` is preferred to `.pivot` or \
`.unstack`; provides same functionality"
.to_string(),
CheckKind::UseOfDotValues => "Use `.to_numpy()` instead of `.values`".to_string(),
CheckKind::UseOfDotReadTable => {
"`.read_csv` is preferred to `.read_table`; provides same functionality".to_string()
}
CheckKind::UseOfDotStack => {
"`.melt` is preferred to `.stack`; provides same functionality".to_string()
}
CheckKind::DfIsABadVariableName => {
"`df` is a bad variable name. Be kinder to your future self.".to_string()
}
CheckKind::UseOfPdMerge => "Use `.merge` method instead of `pd.merge` function. They \
have equivalent functionality."
.to_string(),
// flake8-errmsg
CheckKind::RawStringInException => {
"Exception must not use a string literal, assign to variable first".to_string()
}
CheckKind::FStringInException => {
"Exception must not use an f-string literal, assign to variable first".to_string()
}
CheckKind::DotFormatInException => "Exception must not use a `.format()` string \
directly, assign to variable first"
.to_string(),
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => {
format!(
@@ -2632,6 +2866,41 @@ impl CheckKind {
format!("Unused `noqa` directive for: {codes}")
}
},
// flake8-datetimez
CheckKind::CallDatetimeWithoutTzinfo => "The use of `datetime.datetime()` without \
`tzinfo` argument is not allowed"
.to_string(),
CheckKind::CallDatetimeToday => "The use of `datetime.datetime.today()` is not \
allowed. Use `datetime.datetime.now(tz=)` instead."
.to_string(),
CheckKind::CallDatetimeUtcnow => "The use of `datetime.datetime.utcnow()` is not \
allowed. Use `datetime.datetime.now(tz=)` instead."
.to_string(),
CheckKind::CallDatetimeUtcfromtimestamp => {
"The use of `datetime.datetime.utcfromtimestamp()` is not allowed. Use \
`datetime.datetime.fromtimestamp(, tz=)` instead."
.to_string()
}
CheckKind::CallDatetimeNowWithoutTzinfo => "The use of `datetime.datetime.now()` \
without `tz` argument is not allowed"
.to_string(),
CheckKind::CallDatetimeFromtimestamp => "The use of \
`datetime.datetime.fromtimestamp()` without \
`tz` argument is not allowed"
.to_string(),
CheckKind::CallDatetimeStrptimeWithoutZone => {
"The use of `datetime.datetime.strptime()` without %z must be followed by \
`.replace(tzinfo=)`"
.to_string()
}
CheckKind::CallDateToday => "The use of `datetime.date.today()` is not allowed. Use \
`datetime.datetime.now(tz=).date()` instead."
.to_string(),
CheckKind::CallDateFromtimestamp => {
"The use of `datetime.date.fromtimestamp()` is not allowed. Use \
`datetime.datetime.fromtimestamp(, tz=).date()` instead."
.to_string()
}
}
}
@@ -2651,6 +2920,23 @@ impl CheckKind {
CheckKind::StarArgUnpackingAfterKeywordArg => {
"Star-arg unpacking after a keyword argument is strongly discouraged".to_string()
}
// flake8-datetimez
CheckKind::CallDatetimeToday => {
"The use of `datetime.datetime.today()` is not allowed".to_string()
}
CheckKind::CallDatetimeUtcnow => {
"The use of `datetime.datetime.utcnow()` is not allowed".to_string()
}
CheckKind::CallDatetimeUtcfromtimestamp => {
"The use of `datetime.datetime.utcfromtimestamp()` is not allowed".to_string()
}
CheckKind::CallDateToday => {
"The use of `datetime.date.today()` is not allowed.".to_string()
}
CheckKind::CallDateFromtimestamp => {
"The use of `datetime.date.fromtimestamp()` is not allowed".to_string()
}
_ => self.body(),
}
}
@@ -2682,6 +2968,7 @@ impl CheckKind {
| CheckKind::IsLiteral
| CheckKind::KeyInDict(..)
| CheckKind::MisplacedComparisonConstant(..)
| CheckKind::MissingReturnTypeSpecialMethod(..)
| CheckKind::NewLineAfterLastParagraph
| CheckKind::NewLineAfterSectionName(..)
| CheckKind::NoBlankLineAfterFunction(..)
@@ -2696,13 +2983,15 @@ impl CheckKind {
| CheckKind::NotIsTest
| CheckKind::OneBlankLineAfterClass(..)
| CheckKind::OneBlankLineBeforeClass(..)
| CheckKind::PercentFormatExtraNamedArguments(..)
| CheckKind::PEP3120UnnecessaryCodingComment
| CheckKind::PPrintFound
| CheckKind::PercentFormatExtraNamedArguments(..)
| CheckKind::PrintFound
| CheckKind::RaiseNotImplemented
| CheckKind::RedundantOpenModes
| CheckKind::RedundantTupleInExceptionHandler(..)
| CheckKind::RemoveSixCompat
| CheckKind::DatetimeTimezoneUTC
| CheckKind::SectionNameEndsInColon(..)
| CheckKind::SectionNotOverIndented(..)
| CheckKind::SectionUnderlineAfterName(..)
@@ -2767,6 +3056,81 @@ impl Check {
}
}
/// A hash map from deprecated `CheckCodePrefix` to latest `CheckCodePrefix`.
pub static PREFIX_REDIRECTS: Lazy<FxHashMap<&'static str, CheckCodePrefix>> = Lazy::new(|| {
FxHashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U001", CheckCodePrefix::UP001),
("U003", CheckCodePrefix::UP003),
("U004", CheckCodePrefix::UP004),
("U005", CheckCodePrefix::UP005),
("U006", CheckCodePrefix::UP006),
("U007", CheckCodePrefix::UP007),
("U008", CheckCodePrefix::UP008),
("U009", CheckCodePrefix::UP009),
("U010", CheckCodePrefix::UP010),
("U011", CheckCodePrefix::UP011),
("U012", CheckCodePrefix::UP012),
("U013", CheckCodePrefix::UP013),
("U014", CheckCodePrefix::UP014),
("U015", CheckCodePrefix::UP015),
("U016", CheckCodePrefix::UP016),
("U017", CheckCodePrefix::UP017),
// TODO(charlie): Remove by 2023-02-01.
("I252", CheckCodePrefix::TID252),
("M001", CheckCodePrefix::RUF100),
// TODO(charlie): Remove by 2023-02-01.
("PDV002", CheckCodePrefix::PD002),
("PDV003", CheckCodePrefix::PD003),
("PDV004", CheckCodePrefix::PD004),
("PDV007", CheckCodePrefix::PD007),
("PDV008", CheckCodePrefix::PD008),
("PDV009", CheckCodePrefix::PD009),
("PDV010", CheckCodePrefix::PD010),
("PDV011", CheckCodePrefix::PD011),
("PDV012", CheckCodePrefix::PD012),
("PDV013", CheckCodePrefix::PD013),
("PDV015", CheckCodePrefix::PD015),
("PDV901", CheckCodePrefix::PD901),
// TODO(charlie): Remove by 2023-02-01.
("R501", CheckCodePrefix::RET501),
("R502", CheckCodePrefix::RET502),
("R503", CheckCodePrefix::RET503),
("R504", CheckCodePrefix::RET504),
("R505", CheckCodePrefix::RET505),
("R506", CheckCodePrefix::RET506),
("R507", CheckCodePrefix::RET507),
("R508", CheckCodePrefix::RET508),
("IC001", CheckCodePrefix::ICN001),
("IC002", CheckCodePrefix::ICN001),
("IC003", CheckCodePrefix::ICN001),
("IC004", CheckCodePrefix::ICN001),
// TODO(charlie): Remove by 2023-01-01.
("U", CheckCodePrefix::UP),
("U0", CheckCodePrefix::UP0),
("U00", CheckCodePrefix::UP00),
("U01", CheckCodePrefix::UP01),
// TODO(charlie): Remove by 2023-02-01.
("I2", CheckCodePrefix::TID2),
("I25", CheckCodePrefix::TID25),
("M", CheckCodePrefix::RUF100),
("M0", CheckCodePrefix::RUF100),
// TODO(charlie): Remove by 2023-02-01.
("PDV", CheckCodePrefix::PD),
("PDV0", CheckCodePrefix::PD0),
("PDV01", CheckCodePrefix::PD01),
("PDV9", CheckCodePrefix::PD9),
("PDV90", CheckCodePrefix::PD90),
// TODO(charlie): Remove by 2023-02-01.
("R", CheckCodePrefix::RET),
("R5", CheckCodePrefix::RET5),
("R50", CheckCodePrefix::RET50),
// TODO(charlie): Remove by 2023-02-01.
("IC", CheckCodePrefix::ICN),
("IC0", CheckCodePrefix::ICN0),
])
});
/// A hash map from deprecated to latest `CheckCode`.
pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(|| {
FxHashMap::from_iter([
@@ -2785,25 +3149,38 @@ pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(
("U013", CheckCode::UP013),
("U014", CheckCode::UP014),
("U015", CheckCode::UP015),
("U016", CheckCode::UP016),
("U017", CheckCode::UP017),
// TODO(charlie): Remove by 2023-02-01.
("I252", CheckCode::TID252),
("M001", CheckCode::RUF100),
])
});
/// A hash map from deprecated `CheckCodePrefix` to latest `CheckCodePrefix`.
pub static PREFIX_REDIRECTS: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::new(|| {
FxHashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U", "UP"),
("U0", "UP0"),
("U00", "UP00"),
("U01", "UP01"),
// TODO(charlie): Remove by 2023-02-01.
("I2", "TID2"),
("I25", "TID25"),
("M", "RUF100"),
("M0", "RUF100"),
("PDV002", CheckCode::PD002),
("PDV003", CheckCode::PD003),
("PDV004", CheckCode::PD004),
("PDV007", CheckCode::PD007),
("PDV008", CheckCode::PD008),
("PDV009", CheckCode::PD009),
("PDV010", CheckCode::PD010),
("PDV011", CheckCode::PD011),
("PDV012", CheckCode::PD012),
("PDV013", CheckCode::PD013),
("PDV015", CheckCode::PD015),
("PDV901", CheckCode::PD901),
// TODO(charlie): Remove by 2023-02-01.
("R501", CheckCode::RET501),
("R502", CheckCode::RET502),
("R503", CheckCode::RET503),
("R504", CheckCode::RET504),
("R505", CheckCode::RET505),
("R506", CheckCode::RET506),
("R507", CheckCode::RET507),
("R508", CheckCode::RET508),
// TODO(charlie): Remove by 2023-02-01.
("IC001", CheckCode::ICN001),
("IC002", CheckCode::ICN001),
("IC003", CheckCode::ICN001),
("IC004", CheckCode::ICN001),
])
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use clap::{command, Parser};
use regex::Regex;
@@ -6,6 +6,7 @@ use rustc_hash::FxHashMap;
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::fs;
use crate::logging::LogLevel;
use crate::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat,
@@ -85,10 +86,22 @@ pub struct Cli {
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// Respect file exclusions via `.gitignore` and other standard ignore
/// files.
#[arg(long, overrides_with("no_respect_gitignore"))]
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_show_source"))]
force_exclude: bool,
#[clap(long, overrides_with("force_exclude"), hide = true)]
no_force_exclude: bool,
/// See the files Ruff will be run against with the current settings.
#[arg(long)]
pub show_files: bool,
/// See Ruff's settings.
/// See the settings Ruff will use to check a given Python file.
#[arg(long)]
pub show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
@@ -113,7 +126,7 @@ pub struct Cli {
pub autoformat: bool,
/// The name of the file when passing it through stdin.
#[arg(long)]
pub stdin_filename: Option<String>,
pub stdin_filename: Option<PathBuf>,
/// Explain a rule.
#[arg(long)]
pub explain: Option<CheckCode>,
@@ -155,6 +168,10 @@ impl Cli {
line_length: self.line_length,
max_complexity: self.max_complexity,
per_file_ignores: self.per_file_ignores,
respect_gitignore: resolve_bool_arg(
self.respect_gitignore,
self.no_respect_gitignore,
),
select: self.select,
show_source: resolve_bool_arg(self.show_source, self.no_show_source),
target_version: self.target_version,
@@ -162,6 +179,7 @@ impl Cli {
// TODO(charlie): Included in `pyproject.toml`, but not inherited.
fix: resolve_bool_arg(self.fix, self.no_fix),
format: self.format,
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
},
)
}
@@ -192,12 +210,13 @@ pub struct Arguments {
pub show_files: bool,
pub show_settings: bool,
pub silent: bool,
pub stdin_filename: Option<String>,
pub stdin_filename: Option<PathBuf>,
pub verbose: bool,
pub watch: bool,
}
/// CLI settings that function as configuration overrides.
#[derive(Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct Overrides {
pub dummy_variable_rgx: Option<Regex>,
@@ -210,6 +229,7 @@ pub struct Overrides {
pub line_length: Option<usize>,
pub max_complexity: Option<usize>,
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
pub respect_gitignore: Option<bool>,
pub select: Option<Vec<CheckCodePrefix>>,
pub show_source: Option<bool>,
pub target_version: Option<PythonVersion>,
@@ -217,6 +237,7 @@ pub struct Overrides {
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
pub fix: Option<bool>,
pub format: Option<SerializationFormat>,
pub force_exclude: Option<bool>,
}
/// Map the CLI settings to a `LogLevel`.
@@ -243,6 +264,9 @@ pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgn
}
per_file_ignores
.into_iter()
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes))
.map(|(pattern, prefixes)| {
let absolute = fs::normalize_path(Path::new(&pattern));
PerFileIgnore::new(pattern, absolute, &prefixes)
})
.collect()
}

View File

@@ -1,36 +1,262 @@
use std::path::PathBuf;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::{bail, Result};
use ignore::Error;
use itertools::Itertools;
use log::{debug, error};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use rustpython_ast::Location;
use serde::Serialize;
use walkdir::DirEntry;
use crate::checks::CheckCode;
use crate::fs::iter_python_files;
use crate::autofix::fixer;
use crate::checks::{CheckCode, CheckKind};
use crate::cli::Overrides;
use crate::iterators::par_iter;
use crate::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics};
use crate::message::Message;
use crate::resolver::{FileDiscovery, PyprojectDiscovery};
use crate::settings::flags;
use crate::settings::types::SerializationFormat;
use crate::{Configuration, Settings};
use crate::{packages, resolver};
/// Run the linter over a collection of files.
pub fn run(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
cache: flags::Cache,
autofix: fixer::Mode,
) -> 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 duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
// Discover the package root for each Python file.
let package_roots = packages::detect_package_roots(
&paths
.iter()
.flatten()
.map(ignore::DirEntry::path)
.collect::<Vec<_>>(),
);
let start = Instant::now();
let mut diagnostics: Diagnostics = par_iter(&paths)
.map(|entry| {
match entry {
Ok(entry) => {
let path = entry.path();
let package = path
.parent()
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path, pyproject_strategy);
lint_path(path, package, settings, cache, autofix)
.map_err(|e| (Some(path.to_owned()), e.to_string()))
}
Err(e) => Err((
if let Error::WithPath { path, .. } = e {
Some(path.clone())
} else {
None
},
e.io_error()
.map_or_else(|| e.to_string(), io::Error::to_string),
)),
}
.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
let settings = resolver.resolve(path, pyproject_strategy);
if settings.enabled.contains(&CheckCode::E902) {
Diagnostics::new(vec![Message {
kind: CheckKind::IOError(message),
location: Location::default(),
end_location: Location::default(),
fix: None,
filename: path.to_string_lossy().to_string(),
source: None,
}])
} else {
error!("Failed to check {}: {message}", path.to_string_lossy());
Diagnostics::default()
}
} else {
error!("{message}");
Diagnostics::default()
}
})
})
.reduce(Diagnostics::default, |mut acc, item| {
acc += item;
acc
});
diagnostics.messages.sort_unstable();
let duration = start.elapsed();
debug!("Checked files in: {:?}", duration);
Ok(diagnostics)
}
/// Read a `String` from `stdin`.
fn read_from_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
Ok(buffer)
}
/// Run the linter over a single file, read from `stdin`.
pub fn run_stdin(
filename: Option<&Path>,
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
autofix: fixer::Mode,
) -> Result<Diagnostics> {
if let Some(filename) = filename {
if !resolver::python_file_at_path(filename, pyproject_strategy, file_strategy, overrides)? {
return Ok(Diagnostics::default());
}
}
let settings = match pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings,
PyprojectDiscovery::Hierarchical(settings) => settings,
};
let package_root = filename
.and_then(Path::parent)
.and_then(packages::detect_package_root);
let stdin = read_from_stdin()?;
let mut diagnostics = lint_stdin(filename, package_root, &stdin, settings, autofix)?;
diagnostics.messages.sort_unstable();
Ok(diagnostics)
}
/// Add `noqa` directives to a collection of files.
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 duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications: usize = par_iter(&paths)
.flatten()
.filter_map(|entry| {
let path = entry.path();
let settings = resolver.resolve(path, pyproject_strategy);
match add_noqa_to_path(path, settings) {
Ok(count) => Some(count),
Err(e) => {
error!("Failed to add noqa to {}: {e}", path.to_string_lossy());
None
}
}
})
.sum();
let duration = start.elapsed();
debug!("Added noqa to files in: {:?}", duration);
Ok(modifications)
}
/// Automatically format a collection of files.
pub fn autoformat(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<usize> {
// Collect all the files to format.
let start = Instant::now();
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications = par_iter(&paths)
.flatten()
.filter_map(|entry| {
let path = entry.path();
let settings = resolver.resolve(path, pyproject_strategy);
match autoformat_path(path, settings) {
Ok(()) => Some(()),
Err(e) => {
error!("Failed to autoformat {}: {e}", path.to_string_lossy());
None
}
}
})
.count();
let duration = start.elapsed();
debug!("Auto-formatted files in: {:?}", duration);
Ok(modifications)
}
/// Print the user-facing configuration settings.
pub fn show_settings(
configuration: &Configuration,
project_root: Option<&PathBuf>,
pyproject: Option<&PathBuf>,
) {
println!("Resolved configuration: {configuration:#?}");
println!("Found project root at: {project_root:?}");
println!("Found pyproject.toml at: {pyproject:?}");
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)?;
// Print the list of files.
let Some(entry) = paths
.iter()
.flatten()
.sorted_by(|a, b| a.path().cmp(b.path())).next() else {
bail!("No files found under the given path");
};
let path = entry.path();
let settings = resolver.resolve(path, pyproject_strategy);
println!("Resolved settings for: {path:?}");
println!("{settings:#?}");
Ok(())
}
/// Show the list of files to be checked based on current settings.
pub fn show_files(files: &[PathBuf], settings: &Settings) {
let mut entries: Vec<DirEntry> = files
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)?;
// Print the list of files.
for entry in paths
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flatten()
.collect();
entries.sort_by(|a, b| a.path().cmp(b.path()));
for entry in entries {
.sorted_by(|a, b| a.path().cmp(b.path()))
{
println!("{}", entry.path().to_string_lossy());
}
Ok(())
}
#[derive(Serialize)]
@@ -41,7 +267,7 @@ struct Explanation<'a> {
}
/// Explain a `CheckCode` to the user.
pub fn explain(code: &CheckCode, format: SerializationFormat) -> Result<()> {
pub fn explain(code: &CheckCode, format: &SerializationFormat) -> Result<()> {
match format {
SerializationFormat::Text | SerializationFormat::Grouped => {
println!(

View File

@@ -37,6 +37,7 @@ pub struct IsortDirectives {
}
pub struct Directives {
pub commented_lines: Vec<usize>,
pub noqa_line_for: IntMap<usize, usize>,
pub isort: IsortDirectives,
}
@@ -47,6 +48,7 @@ pub fn extract_directives(
flags: Flags,
) -> Directives {
Directives {
commented_lines: extract_commented_lines(lxr),
noqa_line_for: if flags.contains(Flags::NOQA) {
extract_noqa_line_for(lxr)
} else {
@@ -60,6 +62,16 @@ pub fn extract_directives(
}
}
pub fn extract_commented_lines(lxr: &[LexResult]) -> Vec<usize> {
let mut commented_lines = Vec::new();
for (start, tok, ..) in lxr.iter().flatten() {
if matches!(tok, Tok::Comment) {
commented_lines.push(start.row());
}
}
commented_lines
}
/// Extract a mapping from logical line to noqa line.
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
let mut noqa_line_for: IntMap<usize, usize> = IntMap::default();

View File

@@ -4,6 +4,7 @@ use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checks::{CheckCode, CheckKind};
use crate::eradicate::detection::comment_contains_code;
use crate::settings::flags;
use crate::{Check, Settings, SourceCodeLocator};
fn is_standalone_comment(line: &str) -> bool {
@@ -23,7 +24,7 @@ pub fn commented_out_code(
start: Location,
end: Location,
settings: &Settings,
autofix: bool,
autofix: flags::Autofix,
) -> Option<Check> {
let location = Location::new(start.row(), 0);
let end_location = Location::new(end.row() + 1, 0);
@@ -41,7 +42,9 @@ pub fn commented_out_code(
end_location: end,
},
);
if autofix && settings.fixable.contains(&CheckCode::ERA001) {
if matches!(autofix, flags::Autofix::Enabled)
&& settings.fixable.contains(&CheckCode::ERA001)
{
check.amend(Fix::deletion(location, end_location));
}
Some(check)

View File

@@ -21,7 +21,6 @@ mod tests {
.join(path)
.as_path(),
&settings::Settings::for_rule(check_code),
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);

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