Compare commits

...

48 Commits

Author SHA1 Message Date
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
148 changed files with 6098 additions and 1548 deletions

View File

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

52
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.183-dev.0"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -796,6 +796,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 +894,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 +1845,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.176"
version = "0.0.183"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1841,7 +1865,9 @@ dependencies = [
"fern",
"filetime",
"getrandom 0.2.8",
"glob",
"globset",
"ignore",
"insta",
"itertools",
"libcst",
@@ -1869,12 +1895,13 @@ dependencies = [
"titlecase",
"toml",
"update-informer",
"ureq",
"walkdir",
]
[[package]]
name = "ruff_dev"
version = "0.0.176"
version = "0.0.183"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1892,7 +1919,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.176"
version = "0.0.183"
dependencies = [
"proc-macro2",
"quote",
@@ -1935,7 +1962,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=8d879a53197f9c73062f6160410bdba796a71cbf#8d879a53197f9c73062f6160410bdba796a71cbf"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -1945,7 +1972,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=8d879a53197f9c73062f6160410bdba796a71cbf#8d879a53197f9c73062f6160410bdba796a71cbf"
dependencies = [
"ascii",
"cfg-if 1.0.0",
@@ -1968,7 +1995,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=8d879a53197f9c73062f6160410bdba796a71cbf#8d879a53197f9c73062f6160410bdba796a71cbf"
dependencies = [
"bincode",
"bitflags",
@@ -1985,7 +2012,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=8d879a53197f9c73062f6160410bdba796a71cbf#8d879a53197f9c73062f6160410bdba796a71cbf"
dependencies = [
"ahash",
"anyhow",
@@ -2297,6 +2324,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.183"
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,11 +43,11 @@ 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.183", 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 = "8d879a53197f9c73062f6160410bdba796a71cbf" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "8d879a53197f9c73062f6160410bdba796a71cbf" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "8d879a53197f9c73062f6160410bdba796a71cbf" }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" }
strum = { version = "0.24.1", features = ["strum_macros"] }
@@ -69,6 +71,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"]

239
README.md
View File

@@ -86,6 +86,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)
@@ -94,6 +95,7 @@ of [Conda](https://docs.conda.io/en/latest/):
1. [flake8-tidy-imports (TID)](#flake8-tidy-imports-tid)
1. [flake8-unused-arguments (ARG)](#flake8-unused-arguments-arg)
1. [eradicate (ERA)](#eradicate-era)
1. [pandas-vet (PDV)](#pandas-vet-pdv)
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. -->
@@ -155,7 +157,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.176
rev: v0.0.183
hooks:
- id: ruff
```
@@ -304,13 +306,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 +335,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
@@ -566,6 +619,7 @@ 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 | 🛠 |
### pep8-naming (N)
@@ -731,6 +785,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 +842,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)
@@ -808,6 +872,25 @@ For more, see [eradicate](https://pypi.org/project/eradicate/2.1.0/) on PyPI.
| ---- | ---- | ------- | --- |
| ERA001 | CommentedOutCode | Found commented-out code | 🛠 |
### pandas-vet (PDV)
For more, see [pandas-vet](https://pypi.org/project/pandas-vet/0.2.3/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PDV002 | UseOfInplaceArgument | `inplace=True` should be avoided; it has inconsistent behavior | |
| PDV003 | UseOfDotIsNull | `.isna` is preferred to `.isnull`; functionality is equivalent | |
| PDV004 | UseOfDotNotNull | `.notna` is preferred to `.notnull`; functionality is equivalent | |
| PDV007 | UseOfDotIx | ``ix` i` deprecated; use more explicit `.loc` o` `.iloc` | |
| PDV008 | UseOfDotAt | Use `.loc` instead of `.at`. If speed is important, use numpy. | |
| PDV009 | UseOfDotIat | Use `.iloc` instea` of `.iat`. If speed is important, use numpy. | |
| PDV010 | UseOfDotPivotOrUnstack | `.pivot_table` is preferred to `.pivot` or `.unstack`; provides same functionality | |
| PDV011 | UseOfDotValues | Use `.to_numpy()` instead of `.values` | |
| PDV012 | UseOfDotReadTable | `.read_csv` is preferred to `.read_table`; provides same functionality | |
| PDV013 | UseOfDotStack | `.melt` is preferred to `.stack`; provides same functionality | |
| PDV015 | UseOfPdMerge | Use `.merge` method instead of `pd.merge` function. They have equivalent functionality. | |
| PDV901 | 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.
@@ -852,6 +935,37 @@ For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
Download the [Ruff VS Code extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff).
### Language Server Protocol
Ruff is available as a [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/)
server, distributed 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-server), both of which are
installable via [PyPI](https://pypi.org/project/python-lsp-ruff/):
```shell
pip install python-lsp-server python-lsp-ruff
```
The LSP server can be used with any editor that supports the Language Server Protocol. For example,
to use it with Neovim, you would add something like the following to your `init.lua`:
```lua
require'lspconfig'.pylsp.setup {
settings = {
pylsp = {
plugins = {
ruff = {
enabled = true
}
}
}
},
}
```
[`ruffd`](https://github.com/Seamooo/ruffd) is another implementation of the Language Server
Protocol (LSP) for Ruff, written in Rust.
### PyCharm
Ruff can be installed as an [External Tool](https://www.jetbrains.com/help/pycharm/configuring-third-party-tools.html)
@@ -864,10 +978,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 (LSP) (see:
[Language Server Protocol](#language-server-protocol)).
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 +1040,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 +1087,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)
@@ -995,12 +1106,13 @@ natively, including:
- [`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/)
- [`yesqa`](https://github.com/asottile/yesqa)
- [`eradicate`](https://pypi.org/project/eradicate/)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (1/10)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33)
- [`yesqa`](https://github.com/asottile/yesqa)
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 +1122,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 +1142,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)
@@ -1051,6 +1160,8 @@ 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/),
@@ -1393,7 +1504,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 +1520,30 @@ exclude = [".venv"]
---
#### [`extend`](#extend)
A path to a local `pyproject.toml` file to merge into this configuration.
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`.
@@ -1621,6 +1756,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 +1819,25 @@ 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.
**Default value**: `["."]`
**Type**: `Vec<PathBuf>`
@@ -1815,6 +1987,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)

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.176"
version = "0.0.183"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.176"
version = "0.0.183"
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=8d879a53197f9c73062f6160410bdba796a71cbf#8d879a53197f9c73062f6160410bdba796a71cbf"
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=8d879a53197f9c73062f6160410bdba796a71cbf#8d879a53197f9c73062f6160410bdba796a71cbf"
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=8d879a53197f9c73062f6160410bdba796a71cbf#8d879a53197f9c73062f6160410bdba796a71cbf"
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=8d879a53197f9c73062f6160410bdba796a71cbf#8d879a53197f9c73062f6160410bdba796a71cbf"
dependencies = [
"ahash",
"anyhow",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.176-dev.0"
version = "0.0.183-dev.0"
edition = "2021"
[lib]

View File

@@ -7,7 +7,8 @@ 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::plugin::Plugin;
@@ -73,6 +74,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 +196,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 +220,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);
}
@@ -246,6 +260,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -257,6 +272,7 @@ mod tests {
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -268,6 +284,7 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
@@ -291,6 +308,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -302,6 +320,7 @@ mod tests {
ignore_init_module_imports: None,
line_length: Some(100),
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -313,6 +332,7 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
@@ -336,6 +356,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -347,6 +368,7 @@ mod tests {
ignore_init_module_imports: None,
line_length: Some(100),
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -358,6 +380,7 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
@@ -381,6 +404,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -392,6 +416,7 @@ mod tests {
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -403,6 +428,7 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
@@ -426,6 +452,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -437,6 +464,7 @@ mod tests {
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -448,6 +476,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,
@@ -479,6 +508,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -490,6 +520,7 @@ mod tests {
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::D100,
CheckCodePrefix::D101,
@@ -537,6 +568,7 @@ mod tests {
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
@@ -560,6 +592,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -571,6 +604,7 @@ mod tests {
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![
CheckCodePrefix::E,
CheckCodePrefix::F,
@@ -583,6 +617,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,

View File

@@ -14,6 +14,7 @@ pub enum Plugin {
Flake8Comprehensions,
Flake8Debugger,
Flake8Docstrings,
Flake8ErrMsg,
Flake8Eradicate,
Flake8Print,
Flake8Quotes,
@@ -21,6 +22,7 @@ pub enum Plugin {
Flake8Simplify,
Flake8TidyImports,
McCabe,
PandasVet,
PEP8Naming,
Pyupgrade,
}
@@ -39,12 +41,14 @@ impl FromStr for Plugin {
"flake8-debugger" => Ok(Plugin::Flake8Debugger),
"flake8-docstrings" => Ok(Plugin::Flake8Docstrings),
"flake8-eradicate" => Ok(Plugin::Flake8BlindExcept),
"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}")),
@@ -57,19 +61,24 @@ impl Plugin {
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::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::McCabe => CheckCodePrefix::C9,
// TODO(charlie): Handle rename of `PD` to `PDV`.
Plugin::PandasVet => CheckCodePrefix::PDV,
Plugin::PEP8Naming => CheckCodePrefix::N,
Plugin::Pyupgrade => CheckCodePrefix::U,
}
@@ -101,14 +110,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::PDV],
Plugin::PEP8Naming => vec![CheckCodePrefix::N],
Plugin::Pyupgrade => vec![CheckCodePrefix::U],
Plugin::Pyupgrade => vec![CheckCodePrefix::UP],
}
}
}
@@ -377,6 +388,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);
}
_ => {}
}
}
@@ -398,11 +412,13 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin
Plugin::Flake8Debugger,
Plugin::Flake8Docstrings,
Plugin::Flake8Eradicate,
Plugin::Flake8ErrMsg,
Plugin::Flake8Print,
Plugin::Flake8Quotes,
Plugin::Flake8Return,
Plugin::Flake8Simplify,
Plugin::Flake8TidyImports,
Plugin::PandasVet,
Plugin::PEP8Naming,
Plugin::Pyupgrade,
]

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

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

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,19 @@
from __future__ import annotations
def f_a():
raise RuntimeError("This is an example exception")
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

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

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

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

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

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

View File

@@ -0,0 +1,89 @@
# 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/
Found 7 error(s).
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/src/file.py:1:8: F401 `os` imported but unused
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
6 potentially fixable with the --fix option.
```
Running from the project directory itself should exhibit the same behavior:
```
∴ (cd resources/test/project/ && cargo run .)
Found 7 error(s).
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
src/file.py:1:8: F401 `os` imported but unused
src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
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 .)
Found 2 error(s).
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
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/)
Found 11 error(s).
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/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/src/file.py:1:8: F401 `os` imported but unused
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
11 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 .)
Found 4 error(s).
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
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/
Found 1 error(s).
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
1 potentially fixable with the --fix option.
```

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 @@
[tool.ruff]
src = [".", "python_modules/*"]
exclude = ["examples/excluded"]
extend-select = ["I001"]
extend-ignore = ["F841"]

View File

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

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.176"
version = "0.0.183"
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 = "8d879a53197f9c73062f6160410bdba796a71cbf" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "8d879a53197f9c73062f6160410bdba796a71cbf" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "8d879a53197f9c73062f6160410bdba796a71cbf" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }

View File

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

View File

@@ -1,13 +1,26 @@
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, 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 +162,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 +176,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 +184,32 @@ 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)
}
/// Extract the names of all handled exceptions.
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<Vec<&str>> {
let mut handler_names = vec![];
@@ -229,7 +260,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 +341,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 +553,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

@@ -10,7 +10,7 @@ use crate::autofix::Fix;
use crate::checks::Check;
use crate::source_code_locator::SourceCodeLocator;
#[derive(Hash)]
#[derive(Debug, Hash)]
pub enum Mode {
Generate,
Apply,

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

@@ -6,7 +6,7 @@ use itertools::Itertools;
use log::error;
use nohash_hasher::IntMap;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Location;
use rustpython_ast::{Located, Location};
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
KeywordData, Operator, Stmt, StmtKind, Suite,
@@ -25,6 +25,7 @@ use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{branch_detection, cast, helpers, operations, visitor};
use crate::checks::{Check, CheckCode, CheckKind, DeferralKeyword};
use crate::docstrings::definition::{Definition, DefinitionKind, Docstring, Documentable};
use crate::noqa::Directive;
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
use crate::python::future::ALL_FEATURE_NAMES;
use crate::python::typing;
@@ -37,9 +38,9 @@ use crate::visibility::{module_visibility, transition_scope, Modifier, Visibilit
use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_debugger,
flake8_import_conventions, flake8_print, flake8_return, flake8_simplify, flake8_tidy_imports,
flake8_unused_arguments, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks,
pylint, pyupgrade, visibility,
flake8_errmsg, flake8_import_conventions, flake8_print, flake8_return, flake8_simplify,
flake8_tidy_imports, flake8_unused_arguments, mccabe, noqa, pandas_vet, pep8_naming,
pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, visibility,
};
const GLOBAL_SCOPE_INDEX: usize = 0;
@@ -51,7 +52,9 @@ pub struct Checker<'a> {
// Input data.
path: &'a Path,
autofix: bool,
ignore_noqa: bool,
pub(crate) settings: &'a Settings,
pub(crate) noqa_line_for: &'a IntMap<usize, usize>,
pub(crate) locator: &'a SourceCodeLocator<'a>,
// Computed checks.
checks: Vec<Check>,
@@ -98,13 +101,17 @@ pub struct Checker<'a> {
impl<'a> Checker<'a> {
pub fn new(
settings: &'a Settings,
noqa_line_for: &'a IntMap<usize, usize>,
autofix: bool,
ignore_noqa: bool,
path: &'a Path,
locator: &'a SourceCodeLocator,
) -> Checker<'a> {
Checker {
settings,
noqa_line_for,
autofix,
ignore_noqa,
path,
locator,
checks: vec![],
@@ -199,6 +206,30 @@ impl<'a> Checker<'a> {
matches!(self.bindings[*index].kind, BindingKind::Builtin)
})
}
/// Return `true` if a `CheckCode` is disabled by a `noqa` directive.
pub fn is_ignored(&self, code: &CheckCode, lineno: usize) -> bool {
// TODO(charlie): `noqa` directives are mostly enforced in `check_lines.rs`.
// However, in rare cases, we need to check them here. For example, when
// removing unused imports, we create a single fix that's applied to all
// unused members on a single import. We need to pre-emptively omit any
// members from the fix that will eventually be excluded by a `noqa`.
// Unfortunately, we _do_ want to register a `Check` for each eventually-ignored
// import, so that our `noqa` counts are accurate.
if self.ignore_noqa {
return false;
}
let noqa_lineno = self.noqa_line_for.get(&lineno).unwrap_or(&lineno);
let line = self.locator.slice_source_code_range(&Range {
location: Location::new(*noqa_lineno, 0),
end_location: Location::new(noqa_lineno + 1, 0),
});
match noqa::extract_noqa_directive(&line) {
Directive::None => false,
Directive::All(..) => true,
Directive::Codes(.., codes) => noqa::includes(code, &codes),
}
}
}
impl<'a, 'b> Visitor<'b> for Checker<'a>
@@ -223,10 +254,10 @@ where
StmtKind::Import { .. } => {
self.futures_allowed = false;
}
node => {
_ => {
self.futures_allowed = false;
if !self.seen_import_boundary
&& !helpers::is_assignment_to_a_dunder(node)
&& !helpers::is_assignment_to_a_dunder(stmt)
&& !operations::in_nested_block(
&mut self.parents.iter().rev().map(|node| node.0),
)
@@ -257,12 +288,11 @@ where
}
if self.settings.enabled.contains(&CheckCode::E741) {
let location = Range::from_located(stmt);
self.add_checks(
names
.iter()
.filter_map(|name| {
pycodestyle::checks::ambiguous_variable_name(name, location)
pycodestyle::checks::ambiguous_variable_name(name, stmt)
})
.into_iter(),
);
@@ -309,12 +339,11 @@ where
}
if self.settings.enabled.contains(&CheckCode::E741) {
let location = Range::from_located(stmt);
self.add_checks(
names
.iter()
.filter_map(|name| {
pycodestyle::checks::ambiguous_variable_name(name, location)
pycodestyle::checks::ambiguous_variable_name(name, stmt)
})
.into_iter(),
);
@@ -369,7 +398,8 @@ where
if let Some(check) = pep8_naming::checks::invalid_function_name(
stmt,
name,
&self.settings.pep8_naming,
&self.settings.pep8_naming.ignore_names,
self.locator,
) {
self.add_check(check);
}
@@ -406,9 +436,12 @@ where
}
if self.settings.enabled.contains(&CheckCode::N807) {
if let Some(check) =
pep8_naming::checks::dunder_function_name(self.current_scope(), stmt, name)
{
if let Some(check) = pep8_naming::checks::dunder_function_name(
self.current_scope(),
stmt,
name,
self.locator,
) {
self.add_check(check);
}
}
@@ -445,6 +478,7 @@ where
name,
body,
self.settings.mccabe.max_complexity,
self.locator,
) {
self.add_check(check);
}
@@ -460,7 +494,7 @@ where
pylint::plugins::property_with_parameters(self, stmt, decorator_list, args);
}
self.check_builtin_shadowing(name, Range::from_located(stmt), true);
self.check_builtin_shadowing(name, stmt, true);
// Visit the decorators and arguments, but avoid the body, which will be
// deferred.
@@ -548,7 +582,9 @@ where
}
if self.settings.enabled.contains(&CheckCode::N801) {
if let Some(check) = pep8_naming::checks::invalid_class_name(stmt, name) {
if let Some(check) =
pep8_naming::checks::invalid_class_name(stmt, name, self.locator)
{
self.add_check(check);
}
}
@@ -573,7 +609,7 @@ where
);
}
self.check_builtin_shadowing(name, Range::from_located(stmt), false);
self.check_builtin_shadowing(name, stmt, false);
for expr in bases {
self.visit_expr(expr);
@@ -615,7 +651,7 @@ where
);
} else {
if let Some(asname) = &alias.node.asname {
self.check_builtin_shadowing(asname, Range::from_located(stmt), false);
self.check_builtin_shadowing(asname, stmt, false);
}
// Given `import foo`, `name` and `full_name` would both be `foo`.
@@ -683,7 +719,10 @@ where
if self.settings.enabled.contains(&CheckCode::N811) {
if let Some(check) =
pep8_naming::checks::constant_imported_as_non_constant(
stmt, name, asname,
stmt,
name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -693,7 +732,10 @@ where
if self.settings.enabled.contains(&CheckCode::N812) {
if let Some(check) =
pep8_naming::checks::lowercase_imported_as_non_lowercase(
stmt, name, asname,
stmt,
name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -703,7 +745,10 @@ where
if self.settings.enabled.contains(&CheckCode::N813) {
if let Some(check) =
pep8_naming::checks::camelcase_imported_as_lowercase(
stmt, name, asname,
stmt,
name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -712,7 +757,10 @@ where
if self.settings.enabled.contains(&CheckCode::N814) {
if let Some(check) = pep8_naming::checks::camelcase_imported_as_constant(
stmt, name, asname,
stmt,
name,
asname,
self.locator,
) {
self.add_check(check);
}
@@ -720,7 +768,10 @@ where
if self.settings.enabled.contains(&CheckCode::N817) {
if let Some(check) = pep8_naming::checks::camelcase_imported_as_acronym(
stmt, name, asname,
stmt,
name,
asname,
self.locator,
) {
self.add_check(check);
}
@@ -858,7 +909,7 @@ where
scope.import_starred = true;
} else {
if let Some(asname) = &alias.node.asname {
self.check_builtin_shadowing(asname, Range::from_located(stmt), false);
self.check_builtin_shadowing(asname, stmt, false);
}
// Given `from foo import bar`, `name` would be "bar" and `full_name` would
@@ -927,6 +978,7 @@ where
stmt,
&alias.node.name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -939,6 +991,7 @@ where
stmt,
&alias.node.name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -951,6 +1004,7 @@ where
stmt,
&alias.node.name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -962,6 +1016,7 @@ where
stmt,
&alias.node.name,
asname,
self.locator,
) {
self.add_check(check);
}
@@ -972,6 +1027,7 @@ where
stmt,
&alias.node.name,
asname,
self.locator,
) {
self.add_check(check);
}
@@ -995,6 +1051,20 @@ where
flake8_bugbear::plugins::cannot_raise_literal(self, exc);
}
}
if self.settings.enabled.contains(&CheckCode::EM101)
| self.settings.enabled.contains(&CheckCode::EM102)
| self.settings.enabled.contains(&CheckCode::EM103)
{
if let Some(exc) = exc {
self.add_checks(
flake8_errmsg::checks::check_string_in_exception(
exc,
self.settings.flake8_errmsg.max_string_length,
)
.into_iter(),
);
}
}
}
StmtKind::AugAssign { target, .. } => {
self.handle_node_load(target);
@@ -1107,6 +1177,11 @@ where
self, stmt, targets, value,
);
}
if self.settings.enabled.contains(&CheckCode::PDV901) {
if let Some(check) = pandas_vet::checks::assignment_to_df(targets) {
self.add_check(check);
}
}
}
StmtKind::AnnAssign { target, value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
@@ -1422,15 +1497,14 @@ where
}
ExprContext::Store => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
id,
Range::from_located(expr),
) {
if let Some(check) =
pycodestyle::checks::ambiguous_variable_name(id, expr)
{
self.add_check(check);
}
}
self.check_builtin_shadowing(id, Range::from_located(expr), true);
self.check_builtin_shadowing(id, expr, true);
self.handle_node_store(id, expr);
}
@@ -1458,9 +1532,26 @@ where
pyupgrade::plugins::use_pep585_annotation(self, expr, attr);
}
if self.settings.enabled.contains(&CheckCode::UP016) {
pyupgrade::plugins::remove_six_compat(self, expr);
}
if self.settings.enabled.contains(&CheckCode::YTT202) {
flake8_2020::plugins::name_or_attribute(self, expr);
}
for (code, name) in vec![
(CheckCode::PDV007, "ix"),
(CheckCode::PDV008, "at"),
(CheckCode::PDV009, "iat"),
(CheckCode::PDV011, "values"),
] {
if self.settings.enabled.contains(&code) {
if attr == name {
self.add_check(Check::new(code.kind(), Range::from_located(expr)));
};
}
}
}
ExprKind::Call {
func,
@@ -1533,6 +1624,9 @@ where
if self.settings.enabled.contains(&CheckCode::UP012) {
pyupgrade::plugins::unnecessary_encode_utf8(self, expr, func, args, keywords);
}
if self.settings.enabled.contains(&CheckCode::UP016) {
pyupgrade::plugins::remove_six_compat(self, expr);
}
// flake8-super
if self.settings.enabled.contains(&CheckCode::UP008) {
@@ -1557,14 +1651,7 @@ where
flake8_bugbear::plugins::getattr_with_constant(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::B010) {
if !self
.scope_stack
.iter()
.rev()
.any(|index| matches!(self.scopes[*index].kind, ScopeKind::Lambda(..)))
{
flake8_bugbear::plugins::setattr_with_constant(self, expr, func, args);
}
flake8_bugbear::plugins::setattr_with_constant(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::B022) {
flake8_bugbear::plugins::useless_contextlib_suppress(self, expr, args);
@@ -1838,6 +1925,34 @@ where
}
}
// pandas-vet
if self.settings.enabled.contains(&CheckCode::PDV002) {
self.add_checks(pandas_vet::checks::inplace_argument(keywords).into_iter());
}
for (code, name) in vec![
(CheckCode::PDV003, "isnull"),
(CheckCode::PDV004, "notnull"),
(CheckCode::PDV010, "pivot"),
(CheckCode::PDV010, "unstack"),
(CheckCode::PDV012, "read_table"),
(CheckCode::PDV013, "stack"),
] {
if self.settings.enabled.contains(&code) {
if let ExprKind::Attribute { attr, .. } = &func.node {
if attr == name {
self.add_check(Check::new(code.kind(), Range::from_located(func)));
};
}
}
}
if self.settings.enabled.contains(&CheckCode::PDV015) {
if let Some(check) = pandas_vet::checks::use_of_pd_merge(func) {
self.add_check(check);
};
}
// pygrep-hooks
if self.settings.enabled.contains(&CheckCode::PGH001) {
pygrep_hooks::checks::no_eval(self, func);
@@ -2420,19 +2535,14 @@ where
match name {
Some(name) => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
name,
Range::from_located(excepthandler),
) {
if let Some(check) =
pycodestyle::checks::ambiguous_variable_name(name, excepthandler)
{
self.add_check(check);
}
}
self.check_builtin_shadowing(
name,
Range::from_located(excepthandler),
false,
);
self.check_builtin_shadowing(name, excepthandler, false);
if self.current_scope().values.contains_key(&name.as_str()) {
self.handle_node_store(
@@ -2545,23 +2655,18 @@ where
);
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
&arg.node.arg,
Range::from_located(arg),
) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(&arg.node.arg, arg) {
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::N803) {
if let Some(check) =
pep8_naming::checks::invalid_argument_name(&arg.node.arg, Range::from_located(arg))
{
if let Some(check) = pep8_naming::checks::invalid_argument_name(&arg.node.arg, arg) {
self.add_check(check);
}
}
self.check_builtin_arg_shadowing(&arg.node.arg, Range::from_located(arg));
self.check_builtin_arg_shadowing(&arg.node.arg, arg);
}
}
@@ -3322,6 +3427,8 @@ impl<'a> Checker<'a> {
(&'a RefEquality<'b, Stmt>, Option<&'a RefEquality<'b, Stmt>>);
let mut unused: FxHashMap<BindingContext, Vec<UnusedImport>> = FxHashMap::default();
let mut ignored: FxHashMap<BindingContext, Vec<UnusedImport>> =
FxHashMap::default();
for (name, index) in scope
.values
@@ -3346,12 +3453,21 @@ impl<'a> Checker<'a> {
let defined_by = binding.source.as_ref().unwrap();
let defined_in = self.child_to_parent.get(defined_by);
unused
.entry((defined_by, defined_in))
.or_default()
.push((full_name, &binding.range));
if self.is_ignored(&CheckCode::F401, binding.range.location.row()) {
ignored
.entry((defined_by, defined_in))
.or_default()
.push((full_name, &binding.range));
} else {
unused
.entry((defined_by, defined_in))
.or_default()
.push((full_name, &binding.range));
}
}
let ignore_init =
self.settings.ignore_init_module_imports && self.path.ends_with("__init__.py");
for ((defined_by, defined_in), unused_imports) in unused
.into_iter()
.sorted_by_key(|((defined_by, _), _)| defined_by.0.location)
@@ -3359,17 +3475,15 @@ impl<'a> Checker<'a> {
let child = defined_by.0;
let parent = defined_in.map(|defined_in| defined_in.0);
let ignore_init = self.settings.ignore_init_module_imports
&& self.path.ends_with("__init__.py");
let fix = if !ignore_init && self.patch(&CheckCode::F401) {
let deleted: Vec<&Stmt> =
self.deletions.iter().map(|node| node.0).collect();
match pyflakes::fixes::remove_unused_imports(
self.locator,
&unused_imports,
child,
parent,
&deleted,
self.locator,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
@@ -3397,6 +3511,17 @@ impl<'a> Checker<'a> {
checks.push(check);
}
}
for (_, unused_imports) in ignored
.into_iter()
.sorted_by_key(|((defined_by, _), _)| defined_by.0.location)
{
for (full_name, range) in unused_imports {
checks.push(Check::new(
CheckKind::UnusedImport(full_name.clone(), ignore_init),
*range,
));
}
}
}
}
self.add_checks(checks.into_iter());
@@ -3591,12 +3716,12 @@ impl<'a> Checker<'a> {
}
}
fn check_builtin_shadowing(&mut self, name: &str, location: Range, is_attribute: bool) {
fn check_builtin_shadowing<T>(&mut self, name: &str, located: &Located<T>, is_attribute: bool) {
if is_attribute && matches!(self.current_scope().kind, ScopeKind::Class(_)) {
if self.settings.enabled.contains(&CheckCode::A003) {
if let Some(check) = flake8_builtins::checks::builtin_shadowing(
name,
location,
located,
flake8_builtins::types::ShadowingType::Attribute,
) {
self.add_check(check);
@@ -3606,7 +3731,7 @@ impl<'a> Checker<'a> {
if self.settings.enabled.contains(&CheckCode::A001) {
if let Some(check) = flake8_builtins::checks::builtin_shadowing(
name,
location,
located,
flake8_builtins::types::ShadowingType::Variable,
) {
self.add_check(check);
@@ -3615,11 +3740,11 @@ impl<'a> Checker<'a> {
}
}
fn check_builtin_arg_shadowing(&mut self, name: &str, location: Range) {
fn check_builtin_arg_shadowing(&mut self, name: &str, arg: &Arg) {
if self.settings.enabled.contains(&CheckCode::A002) {
if let Some(check) = flake8_builtins::checks::builtin_shadowing(
name,
location,
arg,
flake8_builtins::types::ShadowingType::Argument,
) {
self.add_check(check);
@@ -3631,11 +3756,13 @@ impl<'a> Checker<'a> {
pub fn check_ast(
python_ast: &Suite,
locator: &SourceCodeLocator,
noqa_line_for: &IntMap<usize, usize>,
settings: &Settings,
autofix: bool,
ignore_noqa: bool,
path: &Path,
) -> Vec<Check> {
let mut checker = Checker::new(settings, autofix, path, locator);
let mut checker = Checker::new(settings, noqa_line_for, autofix, ignore_noqa, path, locator);
checker.push_scope(Scope::new(ScopeKind::Module));
checker.bind_builtins();

View File

@@ -37,7 +37,7 @@ pub fn check_imports(
autofix: bool,
path: &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);
}

View File

@@ -223,6 +223,7 @@ pub enum CheckCode {
UP013,
UP014,
UP015,
UP016,
// pydocstyle
D100,
D101,
@@ -315,6 +316,23 @@ pub enum CheckCode {
RUF100,
// pygrep-hooks
PGH001,
// pandas-vet
PDV002,
PDV003,
PDV004,
PDV007,
PDV008,
PDV009,
PDV010,
PDV011,
PDV012,
PDV013,
PDV015,
PDV901,
// flake8-errmsg
EM101,
EM102,
EM103,
}
#[derive(EnumIter, Debug, PartialEq, Eq)]
@@ -335,6 +353,7 @@ pub enum CheckCategory {
Flake8Builtins,
Flake8Comprehensions,
Flake8Debugger,
Flake8ErrMsg,
Flake8ImportConventions,
Flake8Print,
Flake8Quotes,
@@ -343,6 +362,7 @@ pub enum CheckCategory {
Flake8TidyImports,
Flake8UnusedArguments,
Eradicate,
PandasVet,
PygrepHooks,
Pylint,
Ruff,
@@ -375,6 +395,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",
@@ -384,6 +405,7 @@ impl CheckCategory {
CheckCategory::Flake8UnusedArguments => "flake8-unused-arguments",
CheckCategory::Isort => "isort",
CheckCategory::McCabe => "mccabe",
CheckCategory::PandasVet => "pandas-vet",
CheckCategory::PEP8Naming => "pep8-naming",
CheckCategory::Pycodestyle => "pycodestyle",
CheckCategory::Pydocstyle => "pydocstyle",
@@ -407,6 +429,7 @@ 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],
@@ -415,6 +438,7 @@ impl CheckCategory {
CheckCategory::Flake8UnusedArguments => vec![CheckCodePrefix::ARG],
CheckCategory::Isort => vec![CheckCodePrefix::I],
CheckCategory::McCabe => vec![CheckCodePrefix::C90],
CheckCategory::PandasVet => vec![CheckCodePrefix::PDV],
CheckCategory::PEP8Naming => vec![CheckCodePrefix::N],
CheckCategory::Pycodestyle => vec![CheckCodePrefix::E, CheckCodePrefix::W],
CheckCategory::Pydocstyle => vec![CheckCodePrefix::D],
@@ -473,6 +497,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/",
@@ -504,6 +532,10 @@ impl CheckCategory {
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,
@@ -772,6 +804,7 @@ pub enum CheckKind {
ConvertTypedDictFunctionalToClass(String),
ConvertNamedTupleFunctionalToClass(String),
RedundantOpenModes,
RemoveSixCompat,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -861,6 +894,23 @@ 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),
@@ -1113,6 +1163,7 @@ impl CheckCode {
CheckCode::UP013 => CheckKind::ConvertTypedDictFunctionalToClass("...".to_string()),
CheckCode::UP014 => CheckKind::ConvertNamedTupleFunctionalToClass("...".to_string()),
CheckCode::UP015 => CheckKind::RedundantOpenModes,
CheckCode::UP016 => CheckKind::RemoveSixCompat,
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -1219,6 +1270,23 @@ impl CheckCode {
CheckCode::ICN001 => {
CheckKind::ImportAliasIsNotConventional("...".to_string(), "...".to_string())
}
// pandas-vet
CheckCode::PDV002 => CheckKind::UseOfInplaceArgument,
CheckCode::PDV003 => CheckKind::UseOfDotIsNull,
CheckCode::PDV004 => CheckKind::UseOfDotNotNull,
CheckCode::PDV007 => CheckKind::UseOfDotIx,
CheckCode::PDV008 => CheckKind::UseOfDotAt,
CheckCode::PDV009 => CheckKind::UseOfDotIat,
CheckCode::PDV010 => CheckKind::UseOfDotPivotOrUnstack,
CheckCode::PDV011 => CheckKind::UseOfDotValues,
CheckCode::PDV012 => CheckKind::UseOfDotReadTable,
CheckCode::PDV013 => CheckKind::UseOfDotStack,
CheckCode::PDV015 => CheckKind::UseOfPdMerge,
CheckCode::PDV901 => CheckKind::DfIsABadVariableName,
// flake8-errmsg
CheckCode::EM101 => CheckKind::RawStringInException,
CheckCode::EM102 => CheckKind::FStringInException,
CheckCode::EM103 => CheckKind::DotFormatInException,
// Ruff
CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'),
CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'),
@@ -1354,6 +1422,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,6 +1491,18 @@ impl CheckCode {
CheckCode::N816 => CheckCategory::PEP8Naming,
CheckCode::N817 => CheckCategory::PEP8Naming,
CheckCode::N818 => CheckCategory::PEP8Naming,
CheckCode::PDV002 => CheckCategory::PandasVet,
CheckCode::PDV003 => CheckCategory::PandasVet,
CheckCode::PDV004 => CheckCategory::PandasVet,
CheckCode::PDV007 => CheckCategory::PandasVet,
CheckCode::PDV008 => CheckCategory::PandasVet,
CheckCode::PDV009 => CheckCategory::PandasVet,
CheckCode::PDV010 => CheckCategory::PandasVet,
CheckCode::PDV011 => CheckCategory::PandasVet,
CheckCode::PDV012 => CheckCategory::PandasVet,
CheckCode::PDV013 => CheckCategory::PandasVet,
CheckCode::PDV015 => CheckCategory::PandasVet,
CheckCode::PDV901 => CheckCategory::PandasVet,
CheckCode::PGH001 => CheckCategory::PygrepHooks,
CheckCode::PLC0414 => CheckCategory::Pylint,
CheckCode::PLC2201 => CheckCategory::Pylint,
@@ -1473,6 +1556,7 @@ impl CheckCode {
CheckCode::UP013 => CheckCategory::Pyupgrade,
CheckCode::UP014 => CheckCategory::Pyupgrade,
CheckCode::UP015 => CheckCategory::Pyupgrade,
CheckCode::UP016 => CheckCategory::Pyupgrade,
CheckCode::W292 => CheckCategory::Pycodestyle,
CheckCode::W605 => CheckCategory::Pycodestyle,
CheckCode::YTT101 => CheckCategory::Flake82020,
@@ -1681,6 +1765,7 @@ impl CheckKind {
CheckKind::ConvertTypedDictFunctionalToClass(_) => &CheckCode::UP013,
CheckKind::ConvertNamedTupleFunctionalToClass(_) => &CheckCode::UP014,
CheckKind::RedundantOpenModes => &CheckCode::UP015,
CheckKind::RemoveSixCompat => &CheckCode::UP016,
// pydocstyle
CheckKind::BlankLineAfterLastSection(_) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(_) => &CheckCode::D410,
@@ -1770,6 +1855,23 @@ impl CheckKind {
CheckKind::UnusedLambdaArgument(..) => &CheckCode::ARG005,
// flake8-import-conventions
CheckKind::ImportAliasIsNotConventional(..) => &CheckCode::ICN001,
// pandas-vet
CheckKind::UseOfInplaceArgument => &CheckCode::PDV002,
CheckKind::UseOfDotIsNull => &CheckCode::PDV003,
CheckKind::UseOfDotNotNull => &CheckCode::PDV004,
CheckKind::UseOfDotIx => &CheckCode::PDV007,
CheckKind::UseOfDotAt => &CheckCode::PDV008,
CheckKind::UseOfDotIat => &CheckCode::PDV009,
CheckKind::UseOfDotPivotOrUnstack => &CheckCode::PDV010,
CheckKind::UseOfDotValues => &CheckCode::PDV011,
CheckKind::UseOfDotReadTable => &CheckCode::PDV012,
CheckKind::UseOfDotStack => &CheckCode::PDV013,
CheckKind::UseOfPdMerge => &CheckCode::PDV015,
CheckKind::DfIsABadVariableName => &CheckCode::PDV901,
// flake8-errmsg
CheckKind::RawStringInException => &CheckCode::EM101,
CheckKind::FStringInException => &CheckCode::EM102,
CheckKind::DotFormatInException => &CheckCode::EM103,
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001,
CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002,
@@ -2334,7 +2436,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 +2474,7 @@ 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::ConvertTypedDictFunctionalToClass(name) => {
format!("Convert `{name}` from `TypedDict` functional to class syntax")
}
@@ -2597,6 +2700,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` i` deprecated; use more explicit `.loc` o` `.iloc`".to_string()
}
CheckKind::UseOfDotAt => {
"Use `.loc` instead of `.at`. If speed is important, use numpy.".to_string()
}
CheckKind::UseOfDotIat => {
"Use `.iloc` instea` 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!(
@@ -2669,6 +2817,7 @@ impl CheckKind {
| CheckKind::CommentedOutCode
| CheckKind::ConvertNamedTupleFunctionalToClass(..)
| CheckKind::ConvertTypedDictFunctionalToClass(..)
| CheckKind::RemoveSixCompat
| CheckKind::DashedUnderlineAfterSection(..)
| CheckKind::DeprecatedUnittestAlias(..)
| CheckKind::DoNotAssertFalse
@@ -2785,6 +2934,7 @@ pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(
("U013", CheckCode::UP013),
("U014", CheckCode::UP014),
("U015", CheckCode::UP015),
("U016", CheckCode::UP016),
// TODO(charlie): Remove by 2023-02-01.
("I252", CheckCode::TID252),
("M001", CheckCode::RUF100),

View File

@@ -189,6 +189,12 @@ pub enum CheckCodePrefix {
E902,
E99,
E999,
EM,
EM1,
EM10,
EM101,
EM102,
EM103,
ERA,
ERA0,
ERA00,
@@ -297,6 +303,24 @@ pub enum CheckCodePrefix {
N816,
N817,
N818,
PDV,
PDV0,
PDV00,
PDV002,
PDV003,
PDV004,
PDV007,
PDV008,
PDV009,
PDV01,
PDV010,
PDV011,
PDV012,
PDV013,
PDV015,
PDV9,
PDV90,
PDV901,
PGH,
PGH0,
PGH00,
@@ -416,6 +440,7 @@ pub enum CheckCodePrefix {
U013,
U014,
U015,
U016,
UP,
UP0,
UP00,
@@ -434,6 +459,7 @@ pub enum CheckCodePrefix {
UP013,
UP014,
UP015,
UP016,
W,
W2,
W29,
@@ -1003,6 +1029,12 @@ impl CheckCodePrefix {
CheckCodePrefix::E902 => vec![CheckCode::E902],
CheckCodePrefix::E99 => vec![CheckCode::E999],
CheckCodePrefix::E999 => vec![CheckCode::E999],
CheckCodePrefix::EM => vec![CheckCode::EM101, CheckCode::EM102, CheckCode::EM103],
CheckCodePrefix::EM1 => vec![CheckCode::EM101, CheckCode::EM102, CheckCode::EM103],
CheckCodePrefix::EM10 => vec![CheckCode::EM101, CheckCode::EM102, CheckCode::EM103],
CheckCodePrefix::EM101 => vec![CheckCode::EM101],
CheckCodePrefix::EM102 => vec![CheckCode::EM102],
CheckCodePrefix::EM103 => vec![CheckCode::EM103],
CheckCodePrefix::ERA => vec![CheckCode::ERA001],
CheckCodePrefix::ERA0 => vec![CheckCode::ERA001],
CheckCodePrefix::ERA00 => vec![CheckCode::ERA001],
@@ -1336,6 +1368,62 @@ impl CheckCodePrefix {
CheckCodePrefix::N816 => vec![CheckCode::N816],
CheckCodePrefix::N817 => vec![CheckCode::N817],
CheckCodePrefix::N818 => vec![CheckCode::N818],
CheckCodePrefix::PDV => vec![
CheckCode::PDV002,
CheckCode::PDV003,
CheckCode::PDV004,
CheckCode::PDV007,
CheckCode::PDV008,
CheckCode::PDV009,
CheckCode::PDV010,
CheckCode::PDV011,
CheckCode::PDV012,
CheckCode::PDV013,
CheckCode::PDV015,
CheckCode::PDV901,
],
CheckCodePrefix::PDV0 => vec![
CheckCode::PDV002,
CheckCode::PDV003,
CheckCode::PDV004,
CheckCode::PDV007,
CheckCode::PDV008,
CheckCode::PDV009,
CheckCode::PDV010,
CheckCode::PDV011,
CheckCode::PDV012,
CheckCode::PDV013,
CheckCode::PDV015,
],
CheckCodePrefix::PDV00 => vec![
CheckCode::PDV002,
CheckCode::PDV003,
CheckCode::PDV004,
CheckCode::PDV007,
CheckCode::PDV008,
CheckCode::PDV009,
],
CheckCodePrefix::PDV002 => vec![CheckCode::PDV002],
CheckCodePrefix::PDV003 => vec![CheckCode::PDV003],
CheckCodePrefix::PDV004 => vec![CheckCode::PDV004],
CheckCodePrefix::PDV007 => vec![CheckCode::PDV007],
CheckCodePrefix::PDV008 => vec![CheckCode::PDV008],
CheckCodePrefix::PDV009 => vec![CheckCode::PDV009],
CheckCodePrefix::PDV01 => vec![
CheckCode::PDV010,
CheckCode::PDV011,
CheckCode::PDV012,
CheckCode::PDV013,
CheckCode::PDV015,
],
CheckCodePrefix::PDV010 => vec![CheckCode::PDV010],
CheckCodePrefix::PDV011 => vec![CheckCode::PDV011],
CheckCodePrefix::PDV012 => vec![CheckCode::PDV012],
CheckCodePrefix::PDV013 => vec![CheckCode::PDV013],
CheckCodePrefix::PDV015 => vec![CheckCode::PDV015],
CheckCodePrefix::PDV9 => vec![CheckCode::PDV901],
CheckCodePrefix::PDV90 => vec![CheckCode::PDV901],
CheckCodePrefix::PDV901 => vec![CheckCode::PDV901],
CheckCodePrefix::PGH => vec![CheckCode::PGH001],
CheckCodePrefix::PGH0 => vec![CheckCode::PGH001],
CheckCodePrefix::PGH00 => vec![CheckCode::PGH001],
@@ -1536,6 +1624,7 @@ impl CheckCodePrefix {
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
]
}
CheckCodePrefix::U0 => {
@@ -1560,6 +1649,7 @@ impl CheckCodePrefix {
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
]
}
CheckCodePrefix::U00 => {
@@ -1666,6 +1756,7 @@ impl CheckCodePrefix {
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
]
}
CheckCodePrefix::U010 => {
@@ -1722,6 +1813,15 @@ impl CheckCodePrefix {
);
vec![CheckCode::UP015]
}
CheckCodePrefix::U016 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U016` has been remapped to `UP016`".bold()
);
vec![CheckCode::UP016]
}
CheckCodePrefix::UP => vec![
CheckCode::UP001,
CheckCode::UP003,
@@ -1737,6 +1837,7 @@ impl CheckCodePrefix {
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
],
CheckCodePrefix::UP0 => vec![
CheckCode::UP001,
@@ -1753,6 +1854,7 @@ impl CheckCodePrefix {
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
],
CheckCodePrefix::UP00 => vec![
CheckCode::UP001,
@@ -1779,6 +1881,7 @@ impl CheckCodePrefix {
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
],
CheckCodePrefix::UP010 => vec![CheckCode::UP010],
CheckCodePrefix::UP011 => vec![CheckCode::UP011],
@@ -1786,6 +1889,7 @@ impl CheckCodePrefix {
CheckCodePrefix::UP013 => vec![CheckCode::UP013],
CheckCodePrefix::UP014 => vec![CheckCode::UP014],
CheckCodePrefix::UP015 => vec![CheckCode::UP015],
CheckCodePrefix::UP016 => vec![CheckCode::UP016],
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
CheckCodePrefix::W2 => vec![CheckCode::W292],
CheckCodePrefix::W29 => vec![CheckCode::W292],
@@ -2018,6 +2122,12 @@ impl CheckCodePrefix {
CheckCodePrefix::E902 => SuffixLength::Three,
CheckCodePrefix::E99 => SuffixLength::Two,
CheckCodePrefix::E999 => SuffixLength::Three,
CheckCodePrefix::EM => SuffixLength::Zero,
CheckCodePrefix::EM1 => SuffixLength::One,
CheckCodePrefix::EM10 => SuffixLength::Two,
CheckCodePrefix::EM101 => SuffixLength::Three,
CheckCodePrefix::EM102 => SuffixLength::Three,
CheckCodePrefix::EM103 => SuffixLength::Three,
CheckCodePrefix::ERA => SuffixLength::Zero,
CheckCodePrefix::ERA0 => SuffixLength::One,
CheckCodePrefix::ERA00 => SuffixLength::Two,
@@ -2126,6 +2236,24 @@ impl CheckCodePrefix {
CheckCodePrefix::N816 => SuffixLength::Three,
CheckCodePrefix::N817 => SuffixLength::Three,
CheckCodePrefix::N818 => SuffixLength::Three,
CheckCodePrefix::PDV => SuffixLength::Zero,
CheckCodePrefix::PDV0 => SuffixLength::One,
CheckCodePrefix::PDV00 => SuffixLength::Two,
CheckCodePrefix::PDV002 => SuffixLength::Three,
CheckCodePrefix::PDV003 => SuffixLength::Three,
CheckCodePrefix::PDV004 => SuffixLength::Three,
CheckCodePrefix::PDV007 => SuffixLength::Three,
CheckCodePrefix::PDV008 => SuffixLength::Three,
CheckCodePrefix::PDV009 => SuffixLength::Three,
CheckCodePrefix::PDV01 => SuffixLength::Two,
CheckCodePrefix::PDV010 => SuffixLength::Three,
CheckCodePrefix::PDV011 => SuffixLength::Three,
CheckCodePrefix::PDV012 => SuffixLength::Three,
CheckCodePrefix::PDV013 => SuffixLength::Three,
CheckCodePrefix::PDV015 => SuffixLength::Three,
CheckCodePrefix::PDV9 => SuffixLength::One,
CheckCodePrefix::PDV90 => SuffixLength::Two,
CheckCodePrefix::PDV901 => SuffixLength::Three,
CheckCodePrefix::PGH => SuffixLength::Zero,
CheckCodePrefix::PGH0 => SuffixLength::One,
CheckCodePrefix::PGH00 => SuffixLength::Two,
@@ -2245,6 +2373,7 @@ impl CheckCodePrefix {
CheckCodePrefix::U013 => SuffixLength::Three,
CheckCodePrefix::U014 => SuffixLength::Three,
CheckCodePrefix::U015 => SuffixLength::Three,
CheckCodePrefix::U016 => SuffixLength::Three,
CheckCodePrefix::UP => SuffixLength::Zero,
CheckCodePrefix::UP0 => SuffixLength::One,
CheckCodePrefix::UP00 => SuffixLength::Two,
@@ -2263,6 +2392,7 @@ impl CheckCodePrefix {
CheckCodePrefix::UP013 => SuffixLength::Three,
CheckCodePrefix::UP014 => SuffixLength::Three,
CheckCodePrefix::UP015 => SuffixLength::Three,
CheckCodePrefix::UP016 => SuffixLength::Three,
CheckCodePrefix::W => SuffixLength::Zero,
CheckCodePrefix::W2 => SuffixLength::One,
CheckCodePrefix::W29 => SuffixLength::Two,
@@ -2300,12 +2430,14 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::C,
CheckCodePrefix::D,
CheckCodePrefix::E,
CheckCodePrefix::EM,
CheckCodePrefix::ERA,
CheckCodePrefix::F,
CheckCodePrefix::FBT,
CheckCodePrefix::I,
CheckCodePrefix::ICN,
CheckCodePrefix::N,
CheckCodePrefix::PDV,
CheckCodePrefix::PGH,
CheckCodePrefix::PLC,
CheckCodePrefix::PLE,

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,16 @@ 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,
/// 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.
@@ -155,6 +162,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,
@@ -198,6 +209,7 @@ pub struct Arguments {
}
/// 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 +222,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>,
@@ -243,6 +256,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,238 @@
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;
use crate::resolver::{FileDiscovery, PyprojectDiscovery};
use crate::settings::types::SerializationFormat;
use crate::{Configuration, Settings};
/// Run the linter over a collection of files.
pub fn run(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
cache: bool,
autofix: &fixer::Mode,
) -> Result<Diagnostics> {
// 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 mut diagnostics: Diagnostics = par_iter(&paths)
.map(|entry| {
match entry {
Ok(entry) => {
let path = entry.path();
let settings = resolver.resolve(path, pyproject_strategy);
lint_path(path, settings, &cache.into(), 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(
strategy: &PyprojectDiscovery,
filename: &Path,
autofix: &fixer::Mode,
) -> Result<Diagnostics> {
let stdin = read_from_stdin()?;
let settings = match strategy {
PyprojectDiscovery::Fixed(settings) => settings,
PyprojectDiscovery::Hierarchical(settings) => settings,
};
let mut diagnostics = lint_stdin(filename, &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 +243,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

@@ -8,7 +8,7 @@ expression: checks
row: 29
column: 4
end_location:
row: 35
column: 0
row: 30
column: 16
fix: ~

View File

@@ -8,8 +8,8 @@ expression: checks
row: 4
column: 0
end_location:
row: 9
column: 0
row: 5
column: 8
fix: ~
- kind:
MissingTypeFunctionArgument: a
@@ -35,8 +35,8 @@ expression: checks
row: 9
column: 0
end_location:
row: 14
column: 0
row: 10
column: 8
fix: ~
- kind:
MissingTypeFunctionArgument: b
@@ -62,8 +62,8 @@ expression: checks
row: 19
column: 0
end_location:
row: 24
column: 0
row: 20
column: 8
fix: ~
- kind:
MissingReturnTypePublicFunction: foo
@@ -71,8 +71,8 @@ expression: checks
row: 24
column: 0
end_location:
row: 29
column: 0
row: 25
column: 8
fix: ~
- kind:
DynamicallyTypedExpression: a

View File

@@ -8,8 +8,8 @@ expression: checks
row: 5
column: 4
end_location:
row: 10
column: 0
row: 6
column: 11
fix: ~
- kind:
MissingReturnTypeMagicMethod: __init__
@@ -17,8 +17,8 @@ expression: checks
row: 11
column: 4
end_location:
row: 16
column: 0
row: 12
column: 11
fix: ~
- kind:
MissingReturnTypePrivateFunction: __init__
@@ -26,7 +26,7 @@ expression: checks
row: 40
column: 0
end_location:
row: 42
column: 0
row: 41
column: 7
fix: ~

View File

@@ -8,8 +8,8 @@ expression: checks
row: 45
column: 0
end_location:
row: 50
column: 0
row: 46
column: 15
fix: ~
- kind:
MissingReturnTypePublicFunction: foo
@@ -17,7 +17,7 @@ expression: checks
row: 50
column: 0
end_location:
row: 56
column: 0
row: 55
column: 14
fix: ~

View File

@@ -56,16 +56,23 @@ pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
if KWLIST.contains(&name.as_str()) {
return;
}
let mut check = Check::new(CheckKind::SetAttrWithConstant, Range::from_located(expr));
if checker.patch(check.kind.code()) {
match assignment(obj, name, value) {
Ok(content) => check.amend(Fix::replacement(
content,
expr.location,
expr.end_location.unwrap(),
)),
Err(e) => error!("Failed to fix invalid comparison: {e}"),
};
// We can only replace a `setattr` call (which is an `Expr`) with an assignment
// (which is a `Stmt`) if the `Expr` is already being used as a `Stmt`
// (i.e., it's directly within an `StmtKind::Expr`).
if let StmtKind::Expr { value: child } = &checker.current_parent().0.node {
if expr == child.as_ref() {
let mut check = Check::new(CheckKind::SetAttrWithConstant, Range::from_located(expr));
if checker.patch(check.kind.code()) {
match assignment(obj, name, value) {
Ok(content) => check.amend(Fix::replacement(
content,
expr.location,
expr.end_location.unwrap(),
)),
Err(e) => error!("Failed to fix invalid comparison: {e}"),
};
}
checker.add_check(check);
}
}
checker.add_check(check);
}

View File

@@ -77,4 +77,19 @@ expression: checks
end_location:
row: 22
column: 31
- kind: GetAttrWithConstant
location:
row: 23
column: 3
end_location:
row: 23
column: 20
fix:
content: x.bar
location:
row: 23
column: 3
end_location:
row: 23
column: 20

View File

@@ -4,77 +4,77 @@ expression: checks
---
- kind: SetAttrWithConstant
location:
row: 33
row: 37
column: 0
end_location:
row: 33
row: 37
column: 25
fix:
content: foo.bar = None
location:
row: 33
row: 37
column: 0
end_location:
row: 33
row: 37
column: 25
- kind: SetAttrWithConstant
location:
row: 34
row: 38
column: 0
end_location:
row: 34
row: 38
column: 29
fix:
content: foo._123abc = None
location:
row: 34
row: 38
column: 0
end_location:
row: 34
row: 38
column: 29
- kind: SetAttrWithConstant
location:
row: 35
row: 39
column: 0
end_location:
row: 35
row: 39
column: 28
fix:
content: foo.abc123 = None
location:
row: 35
row: 39
column: 0
end_location:
row: 35
row: 39
column: 28
- kind: SetAttrWithConstant
location:
row: 36
row: 40
column: 0
end_location:
row: 36
row: 40
column: 29
fix:
content: foo.abc123 = None
location:
row: 36
row: 40
column: 0
end_location:
row: 36
row: 40
column: 29
- kind: SetAttrWithConstant
location:
row: 37
row: 41
column: 0
end_location:
row: 37
row: 41
column: 30
fix:
content: foo.bar.baz = None
location:
row: 37
row: 41
column: 0
end_location:
row: 37
row: 41
column: 30

View File

@@ -7,7 +7,7 @@ expression: checks
row: 22
column: 8
end_location:
row: 25
column: 4
row: 23
column: 42
fix: ~

View File

@@ -8,8 +8,8 @@ expression: checks
row: 17
column: 0
end_location:
row: 22
column: 0
row: 19
column: 13
fix: ~
- kind:
AbstractBaseClassWithoutAbstractMethod: MetaBase_1
@@ -17,8 +17,8 @@ expression: checks
row: 58
column: 0
end_location:
row: 63
column: 0
row: 60
column: 13
fix: ~
- kind:
AbstractBaseClassWithoutAbstractMethod: abc_Base_1
@@ -26,8 +26,8 @@ expression: checks
row: 69
column: 0
end_location:
row: 74
column: 0
row: 71
column: 13
fix: ~
- kind:
AbstractBaseClassWithoutAbstractMethod: abc_Base_2
@@ -35,8 +35,8 @@ expression: checks
row: 74
column: 0
end_location:
row: 79
column: 0
row: 76
column: 13
fix: ~
- kind:
AbstractBaseClassWithoutAbstractMethod: notabc_Base_1
@@ -44,8 +44,8 @@ expression: checks
row: 79
column: 0
end_location:
row: 84
column: 0
row: 81
column: 13
fix: ~
- kind:
AbstractBaseClassWithoutAbstractMethod: abc_set_class_variable_4
@@ -53,7 +53,7 @@ expression: checks
row: 128
column: 0
end_location:
row: 130
column: 0
row: 129
column: 7
fix: ~

View File

@@ -8,8 +8,8 @@ expression: checks
row: 15
column: 0
end_location:
row: 22
column: 0
row: 20
column: 9
fix: ~
- kind:
DuplicateTryBlockException: pickle.PickleError
@@ -17,8 +17,8 @@ expression: checks
row: 22
column: 0
end_location:
row: 31
column: 0
row: 29
column: 9
fix: ~
- kind:
DuplicateTryBlockException: TypeError
@@ -26,8 +26,8 @@ expression: checks
row: 31
column: 0
end_location:
row: 39
column: 0
row: 38
column: 9
fix: ~
- kind:
DuplicateTryBlockException: ValueError
@@ -35,7 +35,7 @@ expression: checks
row: 31
column: 0
end_location:
row: 39
column: 0
row: 38
column: 9
fix: ~

View File

@@ -8,8 +8,8 @@ expression: checks
row: 12
column: 4
end_location:
row: 15
column: 4
row: 13
column: 11
fix: ~
- kind:
EmptyMethodWithoutAbstractDecorator: AbstractClass
@@ -17,8 +17,8 @@ expression: checks
row: 15
column: 4
end_location:
row: 18
column: 4
row: 16
column: 12
fix: ~
- kind:
EmptyMethodWithoutAbstractDecorator: AbstractClass
@@ -26,8 +26,8 @@ expression: checks
row: 18
column: 4
end_location:
row: 22
column: 4
row: 20
column: 11
fix: ~
- kind:
EmptyMethodWithoutAbstractDecorator: AbstractClass
@@ -35,7 +35,7 @@ expression: checks
row: 22
column: 4
end_location:
row: 29
column: 4
row: 27
column: 12
fix: ~

View File

@@ -1,10 +1,16 @@
use rustpython_ast::Located;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::flake8_builtins::types::ShadowingType;
use crate::python::builtins::BUILTINS;
/// Check builtin name shadowing.
pub fn builtin_shadowing(name: &str, location: Range, node_type: ShadowingType) -> Option<Check> {
pub fn builtin_shadowing<T>(
name: &str,
located: &Located<T>,
node_type: ShadowingType,
) -> Option<Check> {
if BUILTINS.contains(&name) {
Some(Check::new(
match node_type {
@@ -12,7 +18,7 @@ pub fn builtin_shadowing(name: &str, location: Range, node_type: ShadowingType)
ShadowingType::Argument => CheckKind::BuiltinArgumentShadowing(name.to_string()),
ShadowingType::Attribute => CheckKind::BuiltinAttributeShadowing(name.to_string()),
},
location,
Range::from_located(located),
))
} else {
None

View File

@@ -89,8 +89,8 @@ expression: checks
row: 10
column: 0
end_location:
row: 13
column: 0
row: 11
column: 8
fix: ~
- kind:
BuiltinVariableShadowing: slice
@@ -98,8 +98,8 @@ expression: checks
row: 13
column: 0
end_location:
row: 16
column: 0
row: 14
column: 8
fix: ~
- kind:
BuiltinVariableShadowing: ValueError
@@ -107,8 +107,8 @@ expression: checks
row: 18
column: 0
end_location:
row: 21
column: 0
row: 19
column: 7
fix: ~
- kind:
BuiltinVariableShadowing: memoryview

View File

@@ -17,7 +17,7 @@ expression: checks
row: 7
column: 4
end_location:
row: 9
column: 0
row: 8
column: 12
fix: ~

View File

@@ -0,0 +1,46 @@
use rustpython_ast::{Constant, Expr, ExprKind};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
pub fn check_string_in_exception(exc: &Expr, max_string_length: usize) -> Vec<Check> {
let mut checks = vec![];
if let ExprKind::Call { args, .. } = &exc.node {
if let Some(first) = args.first() {
match &first.node {
// Check for string literals
ExprKind::Constant {
value: Constant::Str(string),
..
} => {
if string.len() > max_string_length {
checks.push(Check::new(
CheckKind::RawStringInException,
Range::from_located(first),
));
}
}
// Check for f-strings
ExprKind::JoinedStr { .. } => checks.push(Check::new(
CheckKind::FStringInException,
Range::from_located(first),
)),
// Check for .format() calls
ExprKind::Call { func, .. } => {
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if attr == "format" && matches!(value.node, ExprKind::Constant { .. }) {
checks.push(Check::new(
CheckKind::DotFormatInException,
Range::from_located(first),
));
}
}
}
_ => {}
}
}
}
checks
}

50
src/flake8_errmsg/mod.rs Normal file
View File

@@ -0,0 +1,50 @@
pub mod checks;
pub mod settings;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::{flake8_errmsg, settings};
#[test]
fn defaults() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_errmsg/EM.py"),
&settings::Settings::for_rules(vec![
CheckCode::EM101,
CheckCode::EM102,
CheckCode::EM103,
]),
false,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!("defaults", checks);
Ok(())
}
#[test]
fn custom() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_errmsg/EM.py"),
&settings::Settings {
flake8_errmsg: flake8_errmsg::settings::Settings {
max_string_length: 20,
},
..settings::Settings::for_rules(vec![
CheckCode::EM101,
CheckCode::EM102,
CheckCode::EM103,
])
},
false,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!("custom", checks);
Ok(())
}
}

View File

@@ -0,0 +1,32 @@
//! Settings for the `flake8-errmsg` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = r#"
Maximum string length for string literals in exception messages.
"#,
default = "0",
value_type = "usize",
example = "max-string-length = 20"
)]
pub max_string_length: Option<usize>,
}
#[derive(Debug, Default, Hash)]
pub struct Settings {
pub max_string_length: usize,
}
impl Settings {
#[allow(clippy::needless_pass_by_value)]
pub fn from_options(options: Options) -> Self {
Self {
max_string_length: options.max_string_length.unwrap_or_default(),
}
}
}

View File

@@ -0,0 +1,29 @@
---
source: src/flake8_errmsg/mod.rs
expression: checks
---
- kind: RawStringInException
location:
row: 5
column: 23
end_location:
row: 5
column: 53
fix: ~
- kind: FStringInException
location:
row: 10
column: 23
end_location:
row: 10
column: 56
fix: ~
- kind: DotFormatInException
location:
row: 14
column: 23
end_location:
row: 14
column: 81
fix: ~

View File

@@ -0,0 +1,29 @@
---
source: src/flake8_errmsg/mod.rs
expression: checks
---
- kind: RawStringInException
location:
row: 5
column: 23
end_location:
row: 5
column: 53
fix: ~
- kind: FStringInException
location:
row: 10
column: 23
end_location:
row: 10
column: 56
fix: ~
- kind: DotFormatInException
location:
row: 14
column: 23
end_location:
row: 14
column: 81
fix: ~

View File

@@ -23,7 +23,12 @@ pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
let defined_in = checker.current_grandparent();
if matches!(defined_by.0.node, StmtKind::Expr { .. }) {
let deleted: Vec<&Stmt> = checker.deletions.iter().map(|node| node.0).collect();
match helpers::remove_stmt(defined_by.0, defined_in.map(|node| node.0), &deleted) {
match helpers::delete_stmt(
defined_by.0,
defined_in.map(|node| node.0),
&deleted,
checker.locator,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
checker.deletions.insert(defined_by.clone());

View File

@@ -7,8 +7,8 @@ expression: checks
row: 7
column: 4
end_location:
row: 12
column: 0
row: 8
column: 16
fix: ~
- kind: ImplicitReturn
location:
@@ -45,8 +45,8 @@ expression: checks
row: 29
column: 8
end_location:
row: 34
column: 0
row: 30
column: 20
fix: ~
- kind: ImplicitReturn
location:

View File

@@ -8,8 +8,8 @@ expression: checks
row: 5
column: 4
end_location:
row: 16
column: 0
row: 13
column: 16
fix: ~
- kind:
SuperfluousElseReturn: Elif
@@ -17,8 +17,8 @@ expression: checks
row: 17
column: 4
end_location:
row: 27
column: 4
row: 26
column: 13
fix: ~
- kind:
SuperfluousElseReturn: Elif
@@ -26,8 +26,8 @@ expression: checks
row: 38
column: 4
end_location:
row: 49
column: 0
row: 46
column: 16
fix: ~
- kind:
SuperfluousElseReturn: Else
@@ -35,8 +35,8 @@ expression: checks
row: 50
column: 4
end_location:
row: 58
column: 0
row: 55
column: 16
fix: ~
- kind:
SuperfluousElseReturn: Else
@@ -44,8 +44,8 @@ expression: checks
row: 61
column: 8
end_location:
row: 67
column: 4
row: 66
column: 20
fix: ~
- kind:
SuperfluousElseReturn: Else
@@ -53,8 +53,8 @@ expression: checks
row: 73
column: 4
end_location:
row: 81
column: 4
row: 80
column: 13
fix: ~
- kind:
SuperfluousElseReturn: Else
@@ -62,8 +62,8 @@ expression: checks
row: 86
column: 8
end_location:
row: 91
column: 4
row: 90
column: 17
fix: ~
- kind:
SuperfluousElseReturn: Else
@@ -71,7 +71,7 @@ expression: checks
row: 97
column: 4
end_location:
row: 109
column: 0
row: 103
column: 23
fix: ~

View File

@@ -8,8 +8,8 @@ expression: checks
row: 5
column: 4
end_location:
row: 16
column: 0
row: 13
column: 26
fix: ~
- kind:
SuperfluousElseRaise: Elif
@@ -17,8 +17,8 @@ expression: checks
row: 17
column: 4
end_location:
row: 27
column: 4
row: 26
column: 13
fix: ~
- kind:
SuperfluousElseRaise: Else
@@ -26,8 +26,8 @@ expression: checks
row: 31
column: 4
end_location:
row: 39
column: 0
row: 36
column: 26
fix: ~
- kind:
SuperfluousElseRaise: Else
@@ -35,8 +35,8 @@ expression: checks
row: 42
column: 8
end_location:
row: 48
column: 4
row: 47
column: 30
fix: ~
- kind:
SuperfluousElseRaise: Else
@@ -44,8 +44,8 @@ expression: checks
row: 54
column: 4
end_location:
row: 62
column: 4
row: 61
column: 13
fix: ~
- kind:
SuperfluousElseRaise: Else
@@ -53,8 +53,8 @@ expression: checks
row: 67
column: 8
end_location:
row: 72
column: 4
row: 71
column: 17
fix: ~
- kind:
SuperfluousElseRaise: Else
@@ -62,7 +62,7 @@ expression: checks
row: 78
column: 4
end_location:
row: 90
column: 0
row: 84
column: 33
fix: ~

View File

@@ -8,8 +8,8 @@ expression: checks
row: 6
column: 8
end_location:
row: 14
column: 0
row: 11
column: 17
fix: ~
- kind:
SuperfluousElseContinue: Elif
@@ -17,8 +17,8 @@ expression: checks
row: 16
column: 8
end_location:
row: 26
column: 8
row: 25
column: 17
fix: ~
- kind:
SuperfluousElseContinue: Else
@@ -26,8 +26,8 @@ expression: checks
row: 34
column: 8
end_location:
row: 40
column: 0
row: 37
column: 17
fix: ~
- kind:
SuperfluousElseContinue: Else
@@ -35,8 +35,8 @@ expression: checks
row: 44
column: 12
end_location:
row: 50
column: 8
row: 49
column: 24
fix: ~
- kind:
SuperfluousElseContinue: Else
@@ -44,8 +44,8 @@ expression: checks
row: 57
column: 8
end_location:
row: 65
column: 8
row: 64
column: 17
fix: ~
- kind:
SuperfluousElseContinue: Else
@@ -53,8 +53,8 @@ expression: checks
row: 71
column: 12
end_location:
row: 76
column: 8
row: 75
column: 21
fix: ~
- kind:
SuperfluousElseContinue: Else
@@ -62,7 +62,7 @@ expression: checks
row: 83
column: 8
end_location:
row: 92
column: 0
row: 89
column: 24
fix: ~

View File

@@ -8,8 +8,8 @@ expression: checks
row: 6
column: 8
end_location:
row: 14
column: 0
row: 11
column: 17
fix: ~
- kind:
SuperfluousElseBreak: Elif
@@ -17,8 +17,8 @@ expression: checks
row: 16
column: 8
end_location:
row: 26
column: 8
row: 25
column: 17
fix: ~
- kind:
SuperfluousElseBreak: Else
@@ -26,8 +26,8 @@ expression: checks
row: 31
column: 8
end_location:
row: 37
column: 0
row: 34
column: 17
fix: ~
- kind:
SuperfluousElseBreak: Else
@@ -35,8 +35,8 @@ expression: checks
row: 41
column: 12
end_location:
row: 47
column: 8
row: 46
column: 21
fix: ~
- kind:
SuperfluousElseBreak: Else
@@ -44,8 +44,8 @@ expression: checks
row: 54
column: 8
end_location:
row: 62
column: 8
row: 61
column: 17
fix: ~
- kind:
SuperfluousElseBreak: Else
@@ -53,8 +53,8 @@ expression: checks
row: 68
column: 12
end_location:
row: 73
column: 8
row: 72
column: 21
fix: ~
- kind:
SuperfluousElseBreak: Else
@@ -62,7 +62,7 @@ expression: checks
row: 80
column: 8
end_location:
row: 92
column: 0
row: 86
column: 21
fix: ~

172
src/fs.rs
View File

@@ -5,15 +5,13 @@ use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use globset::GlobMatcher;
use log::debug;
use path_absolutize::{path_dedot, Absolutize};
use rustc_hash::FxHashSet;
use walkdir::{DirEntry, WalkDir};
use crate::checks::CheckCode;
/// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
pub fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
let file_path = path
.to_str()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
@@ -25,62 +23,7 @@ fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
Ok((file_path, file_basename))
}
fn is_excluded(file_path: &str, file_basename: &str, exclude: &globset::GlobSet) -> bool {
exclude.is_match(file_path) || exclude.is_match(file_basename)
}
fn is_included(path: &Path) -> bool {
let file_name = path.to_string_lossy();
file_name.ends_with(".py") || file_name.ends_with(".pyi")
}
pub fn iter_python_files<'a>(
path: &'a Path,
exclude: &'a globset::GlobSet,
extend_exclude: &'a globset::GlobSet,
) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> + 'a {
// Run some checks over the provided patterns, to enable optimizations below.
let has_exclude = !exclude.is_empty();
let has_extend_exclude = !extend_exclude.is_empty();
WalkDir::new(normalize_path(path))
.into_iter()
.filter_entry(move |entry| {
if !has_exclude && !has_extend_exclude {
return true;
}
let path = entry.path();
match extract_path_names(path) {
Ok((file_path, file_basename)) => {
if has_exclude && is_excluded(file_path, file_basename, exclude) {
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if has_extend_exclude
&& is_excluded(file_path, file_basename, extend_exclude)
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
} else {
true
}
}
Err(e) => {
debug!("Ignored path due to error in parsing: {:?}: {}", path, e);
true
}
}
})
.filter(|entry| {
entry.as_ref().map_or(true, |entry| {
(entry.depth() == 0 || is_included(entry.path()))
&& !entry.file_type().is_dir()
&& !(entry.file_type().is_symlink() && entry.path().is_dir())
})
})
}
/// Create tree set with codes matching the pattern/code pairs.
/// Create a set with codes matching the pattern/code pairs.
pub(crate) fn ignores_from_path<'a>(
path: &Path,
pattern_code_pairs: &'a [(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)],
@@ -128,114 +71,3 @@ pub(crate) fn read_file(path: &Path) -> Result<String> {
buf_reader.read_to_string(&mut contents)?;
Ok(contents)
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use anyhow::Result;
use globset::GlobSet;
use path_absolutize::Absolutize;
use crate::fs::{extract_path_names, is_excluded, is_included};
use crate::settings::types::FilePattern;
#[test]
fn inclusions() {
let path = Path::new("foo/bar/baz.py").absolutize().unwrap();
assert!(is_included(&path));
let path = Path::new("foo/bar/baz.pyi").absolutize().unwrap();
assert!(is_included(&path));
let path = Path::new("foo/bar/baz.js").absolutize().unwrap();
assert!(!is_included(&path));
let path = Path::new("foo/bar/baz").absolutize().unwrap();
assert!(!is_included(&path));
}
fn make_exclusion(file_pattern: FilePattern, project_root: Option<&PathBuf>) -> GlobSet {
let mut builder = globset::GlobSetBuilder::new();
file_pattern.add_to(&mut builder, project_root).unwrap();
builder.build().unwrap()
}
#[test]
fn exclusions() -> Result<()> {
let project_root = Path::new("/tmp/");
let path = Path::new("foo").absolutize_from(project_root).unwrap();
let exclude = FilePattern::User("foo".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = FilePattern::User("bar".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = FilePattern::User("baz.py".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = FilePattern::User("foo/bar".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = FilePattern::User("foo/bar/baz.py".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = FilePattern::User("foo/bar/*.py".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = FilePattern::User("baz".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(!is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
Ok(())
}
}

55
src/isort/helpers.rs Normal file
View File

@@ -0,0 +1,55 @@
use rustpython_ast::Stmt;
use crate::source_code_locator::SourceCodeLocator;
/// Return `true` if a `Stmt` is preceded by a "comment break"
pub fn has_comment_break(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
// Starting from the `Stmt` (`def f(): pass`), we want to detect patterns like
// this:
//
// import os
//
// # Detached comment.
//
// def f(): pass
// This should also be detected:
//
// import os
//
// # Detached comment.
//
// # Direct comment.
// def f(): pass
// But this should not:
//
// import os
//
// # Direct comment.
// def f(): pass
let mut seen_blank = false;
for line in locator
.slice_source_code_until(&stmt.location)
.lines()
.rev()
{
let line = line.trim();
if seen_blank {
if line.starts_with('#') {
return true;
} else if !line.is_empty() {
break;
}
} else {
if line.is_empty() {
seen_blank = true;
} else if line.starts_with('#') || line.starts_with('@') {
continue;
} else {
break;
}
}
}
false
}

View File

@@ -18,6 +18,7 @@ use crate::isort::types::{
mod categorize;
mod comments;
pub mod format;
mod helpers;
pub mod plugins;
pub mod settings;
mod sorting;

View File

@@ -1,7 +1,9 @@
use rustpython_ast::{Location, Stmt};
use textwrap::{dedent, indent};
use crate::ast::helpers::{count_trailing_lines, match_leading_content, match_trailing_content};
use crate::ast::helpers::{
count_trailing_lines, followed_by_multi_statement_line, preceded_by_multi_statement_line,
};
use crate::ast::types::Range;
use crate::ast::whitespace::leading_space;
use crate::autofix::Fix;
@@ -39,6 +41,14 @@ pub fn check_imports(
let range = extract_range(&block.imports);
// Special-cases: there's leading or trailing content in the import block. These
// are too hard to get right, and relatively rare, so flag but don't fix.
if preceded_by_multi_statement_line(block.imports.first().unwrap(), locator)
|| followed_by_multi_statement_line(block.imports.last().unwrap(), locator)
{
return Some(Check::new(CheckKind::UnsortedImports, range));
}
// Extract comments. Take care to grab any inline comments from the last line.
let comments = comments::collect_comments(
&Range {
@@ -48,9 +58,6 @@ pub fn check_imports(
locator,
);
// Special-cases: there's leading or trailing content in the import block.
let has_leading_content = match_leading_content(block.imports.first().unwrap(), locator);
let has_trailing_content = match_trailing_content(block.imports.last().unwrap(), locator);
let num_trailing_lines = if block.trailer.is_none() {
0
} else {
@@ -70,46 +77,23 @@ pub fn check_imports(
settings.isort.force_wrap_aliases,
);
if has_leading_content || has_trailing_content {
// Expand the span the entire range, including leading and trailing space.
let range = Range {
location: Location::new(range.location.row(), 0),
end_location: Location::new(range.end_location.row() + 1 + num_trailing_lines, 0),
};
let actual = dedent(&locator.slice_source_code_range(&range));
if actual == expected {
None
} else {
let mut check = Check::new(CheckKind::UnsortedImports, range);
if autofix && settings.fixable.contains(check.kind.code()) {
let mut content = String::new();
if has_leading_content {
content.push('\n');
}
content.push_str(&indent(&expected, indentation));
check.amend(Fix::replacement(
content,
// Preserve leading prefix (but put the imports on a new line).
if has_leading_content {
range.location
} else {
Location::new(range.location.row(), 0)
},
// TODO(charlie): Preserve trailing suffixes. Right now, we strip them.
Location::new(range.end_location.row() + 1 + num_trailing_lines, 0),
indent(&expected, indentation),
range.location,
range.end_location,
));
}
Some(check)
} else {
// Expand the span the entire range, including leading and trailing space.
let range = Range {
location: Location::new(range.location.row(), 0),
end_location: Location::new(range.end_location.row() + 1 + num_trailing_lines, 0),
};
let actual = dedent(&locator.slice_source_code_range(&range));
if actual == expected {
None
} else {
let mut check = Check::new(CheckKind::UnsortedImports, range);
if autofix && settings.fixable.contains(check.kind.code()) {
check.amend(Fix::replacement(
indent(&expected, indentation),
range.location,
range.end_location,
));
}
Some(check)
}
}
}

View File

@@ -49,32 +49,17 @@ expression: checks
column: 0
- kind: UnsortedImports
location:
row: 33
row: 52
column: 0
end_location:
row: 35
row: 54
column: 0
fix:
content: " import collections\n import typing\n\n"
content: "import os\n\n\n"
location:
row: 33
row: 52
column: 0
end_location:
row: 35
column: 0
- kind: UnsortedImports
location:
row: 39
column: 0
end_location:
row: 41
column: 0
fix:
content: " import collections\n import typing\n\n"
location:
row: 39
column: 0
end_location:
row: 41
row: 54
column: 0

View File

@@ -47,19 +47,4 @@ expression: checks
end_location:
row: 16
column: 0
- kind: UnsortedImports
location:
row: 33
column: 0
end_location:
row: 35
column: 0
fix:
content: " import collections\n import typing\n\n"
location:
row: 33
column: 0
end_location:
row: 35
column: 0

View File

@@ -9,14 +9,7 @@ expression: checks
end_location:
row: 2
column: 9
fix:
content: "\nimport os\nimport sys\n\n"
location:
row: 1
column: 7
end_location:
row: 4
column: 0
fix: ~
- kind: UnsortedImports
location:
row: 5
@@ -24,12 +17,21 @@ expression: checks
end_location:
row: 6
column: 13
fix:
content: "\n import os\n import sys\n"
location:
row: 5
column: 11
end_location:
row: 7
column: 0
fix: ~
- kind: UnsortedImports
location:
row: 10
column: 8
end_location:
row: 10
column: 17
fix: ~
- kind: UnsortedImports
location:
row: 13
column: 0
end_location:
row: 13
column: 9
fix: ~

View File

@@ -9,14 +9,7 @@ expression: checks
end_location:
row: 2
column: 9
fix:
content: "import os\nimport sys\n\n"
location:
row: 1
column: 0
end_location:
row: 4
column: 0
fix: ~
- kind: UnsortedImports
location:
row: 5
@@ -24,12 +17,5 @@ expression: checks
end_location:
row: 6
column: 13
fix:
content: " import os\n import sys\n\n"
location:
row: 5
column: 0
end_location:
row: 7
column: 0
fix: ~

View File

@@ -8,20 +8,24 @@ use rustpython_ast::{
use crate::ast::visitor::Visitor;
use crate::directives::IsortDirectives;
use crate::isort::helpers;
use crate::source_code_locator::SourceCodeLocator;
#[derive(Debug)]
pub enum Trailer {
Sibling,
ClassDef,
FunctionDef,
}
#[derive(Default)]
#[derive(Debug, Default)]
pub struct Block<'a> {
pub imports: Vec<&'a Stmt>,
pub trailer: Option<Trailer>,
}
pub struct ImportTracker<'a> {
locator: &'a SourceCodeLocator<'a>,
directives: &'a IsortDirectives,
pyi: bool,
blocks: Vec<Block<'a>>,
@@ -30,8 +34,13 @@ pub struct ImportTracker<'a> {
}
impl<'a> ImportTracker<'a> {
pub fn new(directives: &'a IsortDirectives, path: &'a Path) -> Self {
pub fn new(
locator: &'a SourceCodeLocator<'a>,
directives: &'a IsortDirectives,
path: &'a Path,
) -> Self {
Self {
locator,
directives,
pyi: path.extension().map_or(false, |ext| ext == "pyi"),
blocks: vec![Block::default()],
@@ -46,31 +55,46 @@ impl<'a> ImportTracker<'a> {
}
fn trailer_for(&self, stmt: &'a Stmt) -> Option<Trailer> {
if self.pyi {
// Black treats interface files differently, limiting to one newline
// (`Trailing::Sibling`), and avoiding inserting any newlines in nested function
// blocks.
if self.nested
&& matches!(
stmt.node,
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. }
)
{
None
} else {
Some(Trailer::Sibling)
}
} else if self.nested {
Some(Trailer::Sibling)
} else {
Some(match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
Trailer::FunctionDef
}
StmtKind::ClassDef { .. } => Trailer::ClassDef,
_ => Trailer::Sibling,
})
// No need to compute trailers if we won't be finalizing anything.
let index = self.blocks.len() - 1;
if self.blocks[index].imports.is_empty() {
return None;
}
// Similar to isort, avoid enforcing any newline behaviors in nested blocks.
if self.nested {
return None;
}
Some(if self.pyi {
// Black treats interface files differently, limiting to one newline
// (`Trailing::Sibling`).
Trailer::Sibling
} else {
// If the import block is followed by a class or function, we want to enforce
// two blank lines. The exception: if, between the import and the class or
// function, we have at least one commented line, followed by at
// least one blank line. In that case, we treat it as a regular
// sibling (i.e., as if the comment is the next statement, as
// opposed to the class or function).
match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
if helpers::has_comment_break(stmt, self.locator) {
Trailer::Sibling
} else {
Trailer::FunctionDef
}
}
StmtKind::ClassDef { .. } => {
if helpers::has_comment_break(stmt, self.locator) {
Trailer::Sibling
} else {
Trailer::ClassDef
}
}
_ => Trailer::Sibling,
}
})
}
fn finalize(&mut self, trailer: Option<Trailer>) {

16
src/iterators.rs Normal file
View File

@@ -0,0 +1,16 @@
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
/// Shim that calls `par_iter` except for wasm because there's no wasm support
/// in rayon yet (there is a shim to be used for the web, but it requires js
/// cooperation) Unfortunately, `ParallelIterator` does not implement `Iterator`
/// so the signatures diverge
#[cfg(not(target_family = "wasm"))]
pub fn par_iter<T: Sync>(iterable: &[T]) -> impl ParallelIterator<Item = &T> {
iterable.par_iter()
}
#[cfg(target_family = "wasm")]
pub fn par_iter<T: Sync>(iterable: &[T]) -> impl Iterator<Item = &T> {
iterable.iter()
}

View File

@@ -14,13 +14,14 @@
use std::path::Path;
use anyhow::Result;
use log::debug;
use path_absolutize::path_dedot;
use rustpython_helpers::tokenize;
use rustpython_parser::lexer::LexResult;
use settings::{pyproject, Settings};
use crate::checks::Check;
use crate::linter::check_path;
use crate::resolver::Relativity;
use crate::settings::configuration::Configuration;
use crate::source_code_locator::SourceCodeLocator;
@@ -49,6 +50,7 @@ pub mod flake8_bugbear;
mod flake8_builtins;
mod flake8_comprehensions;
mod flake8_debugger;
pub mod flake8_errmsg;
mod flake8_import_conventions;
mod flake8_print;
pub mod flake8_quotes;
@@ -58,12 +60,14 @@ pub mod flake8_tidy_imports;
mod flake8_unused_arguments;
pub mod fs;
mod isort;
pub mod iterators;
mod lex;
pub mod linter;
pub mod logging;
pub mod mccabe;
pub mod message;
mod noqa;
mod pandas_vet;
pub mod pep8_naming;
pub mod printer;
mod pycodestyle;
@@ -73,6 +77,7 @@ mod pygrep_hooks;
mod pylint;
mod python;
mod pyupgrade;
pub mod resolver;
mod ruff;
mod rustpython_helpers;
pub mod settings;
@@ -82,24 +87,24 @@ pub mod updates;
mod vendored;
pub mod visibility;
/// Load the relevant `Settings` for a given `Path`.
fn resolve(path: &Path) -> Result<Settings> {
if let Some(pyproject) = pyproject::find_pyproject_toml(path)? {
// First priority: `pyproject.toml` in the current `Path`.
resolver::resolve_settings(&pyproject, &Relativity::Parent, None)
} else if let Some(pyproject) = pyproject::find_user_pyproject_toml() {
// Second priority: user-specific `pyproject.toml`.
resolver::resolve_settings(&pyproject, &Relativity::Cwd, None)
} else {
// Fallback: default settings.
Settings::from_configuration(Configuration::default(), &path_dedot::CWD)
}
}
/// Run Ruff over Python source code directly.
pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
// Find the project root and pyproject.toml.
let project_root = pyproject::find_project_root(&[path.to_path_buf()]);
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
let pyproject = pyproject::find_pyproject_toml(project_root.as_ref());
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings = Settings::from_configuration(
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?,
project_root.as_ref(),
)?;
// Load the relevant `Settings` for the given `Path`.
let settings = resolve(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);

View File

@@ -83,7 +83,15 @@ pub(crate) fn check_path(
match rustpython_helpers::parse_program_tokens(tokens, "<filename>") {
Ok(python_ast) => {
if use_ast {
checks.extend(check_ast(&python_ast, locator, settings, autofix, path));
checks.extend(check_ast(
&python_ast,
locator,
&directives.noqa_line_for,
settings,
autofix,
ignore_noqa,
path,
));
}
if use_imports {
checks.extend(check_imports(
@@ -211,7 +219,7 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
}
/// Apply autoformatting to the source code at the given `Path`.
pub fn autoformat_path(path: &Path) -> Result<()> {
pub fn autoformat_path(path: &Path, _settings: &Settings) -> Result<()> {
// Read the file from disk.
let contents = fs::read_file(path)?;

View File

@@ -11,190 +11,64 @@
clippy::too_many_lines
)]
use std::io::{self, Read};
use std::io::{self};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::Instant;
use ::ruff::autofix::fixer;
use ::ruff::checks::{CheckCode, CheckKind};
use ::ruff::cli::{extract_log_level, Cli};
use ::ruff::fs::iter_python_files;
use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics};
use ::ruff::logging::{set_up_logging, LogLevel};
use ::ruff::message::Message;
use ::ruff::printer::Printer;
use ::ruff::resolver::PyprojectDiscovery;
use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::types::SerializationFormat;
use ::ruff::settings::{pyproject, Settings};
#[cfg(feature = "update-informer")]
use ::ruff::updates;
use ::ruff::{cache, commands, fs};
use ::ruff::{cache, commands};
use anyhow::Result;
use clap::{CommandFactory, Parser};
use colored::Colorize;
use log::{debug, error};
use notify::{recommended_watcher, RecursiveMode, Watcher};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use rustpython_ast::Location;
use walkdir::DirEntry;
use path_absolutize::path_dedot;
use ruff::cli::Overrides;
use ruff::resolver::{resolve_settings, FileDiscovery, Relativity};
/// Shim that calls `par_iter` except for wasm because there's no wasm support
/// in rayon yet (there is a shim to be used for the web, but it requires js
/// cooperation) Unfortunately, `ParallelIterator` does not implement `Iterator`
/// so the signatures diverge
#[cfg(not(target_family = "wasm"))]
fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl ParallelIterator<Item = &T> {
iterable.par_iter()
}
#[cfg(target_family = "wasm")]
fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl Iterator<Item = &T> {
iterable.iter()
}
fn read_from_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
Ok(buffer)
}
fn run_once_stdin(
settings: &Settings,
filename: &Path,
autofix: &fixer::Mode,
) -> Result<Diagnostics> {
let stdin = read_from_stdin()?;
let mut diagnostics = lint_stdin(filename, &stdin, settings, autofix)?;
diagnostics.messages.sort_unstable();
Ok(diagnostics)
}
fn run_once(
files: &[PathBuf],
settings: &Settings,
cache: bool,
autofix: &fixer::Mode,
) -> Diagnostics {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<Result<DirEntry, walkdir::Error>> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let mut diagnostics: Diagnostics = par_iter(&paths)
.map(|entry| {
match entry {
Ok(entry) => {
let path = entry.path();
lint_path(path, settings, &cache.into(), autofix)
.map_err(|e| (Some(path.to_owned()), e.to_string()))
}
Err(e) => Err((
e.path().map(Path::to_owned),
e.io_error()
.map_or_else(|| e.to_string(), io::Error::to_string),
)),
}
.unwrap_or_else(|(path, message)| {
if let Some(path) = path {
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);
diagnostics
}
fn add_noqa(files: &[PathBuf], settings: &Settings) -> usize {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flatten()
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications: usize = par_iter(&paths)
.filter_map(|entry| {
let path = entry.path();
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);
modifications
}
fn autoformat(files: &[PathBuf], settings: &Settings) -> usize {
// Collect all the files to format.
let start = Instant::now();
let paths: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flatten()
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications = par_iter(&paths)
.filter_map(|entry| {
let path = entry.path();
match autoformat_path(path) {
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);
modifications
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<PyprojectDiscovery> {
if let Some(pyproject) = config {
// First priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
let settings = resolve_settings(&pyproject, &Relativity::Cwd, Some(overrides))?;
Ok(PyprojectDiscovery::Fixed(settings))
} else if let Some(pyproject) = pyproject::find_pyproject_toml(path_dedot::CWD.as_path())? {
// Second priority: find a `pyproject.toml` file in the current working path,
// and resolve all paths relative to that directory. (With
// `Strategy::Hierarchical`, we'll end up finding the "closest" `pyproject.toml`
// file for every Python file later on, so these act as the "default" settings.)
let settings = resolve_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
Ok(PyprojectDiscovery::Hierarchical(settings))
} else if let Some(pyproject) = pyproject::find_user_pyproject_toml() {
// Third priority: find a user-specific `pyproject.toml`, but resolve all paths
// relative the current working directory. (With `Strategy::Hierarchical`, we'll
// end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.)
let settings = resolve_settings(&pyproject, &Relativity::Cwd, Some(overrides))?;
Ok(PyprojectDiscovery::Hierarchical(settings))
} else {
// Fallback: load Ruff's default settings, and resolve all paths relative to the
// current working directory. (With `Strategy::Hierarchical`, we'll end up the
// "closest" `pyproject.toml` file for every Python file later on, so these act
// as the "default" settings.)
let mut config = Configuration::default();
// Apply command-line options that override defaults.
config.apply(overrides.clone());
let settings = Settings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
}
}
fn inner_main() -> Result<ExitCode> {
@@ -203,63 +77,48 @@ fn inner_main() -> Result<ExitCode> {
let log_level = extract_log_level(&cli);
set_up_logging(&log_level)?;
if let Some(shell) = cli.generate_shell_completion {
shell.generate(&mut Cli::command(), &mut std::io::stdout());
return Ok(ExitCode::SUCCESS);
}
// Find the project root and pyproject.toml.
let config: Option<PathBuf> = cli.config;
let project_root = config.as_ref().map_or_else(
|| pyproject::find_project_root(&cli.files),
|config| config.parent().map(fs::normalize_path),
);
let pyproject = config.or_else(|| pyproject::find_pyproject_toml(project_root.as_ref()));
// Reconcile configuration from pyproject.toml and command-line arguments.
let mut configuration =
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?;
configuration.merge(overrides);
if cli.show_settings && cli.show_files {
eprintln!("Error: specify --show-settings or show-files (not both).");
return Ok(ExitCode::FAILURE);
anyhow::bail!("specify --show-settings or show-files (not both)")
}
if cli.show_settings {
commands::show_settings(&configuration, project_root.as_ref(), pyproject.as_ref());
if let Some(shell) = cli.generate_shell_completion {
shell.generate(&mut Cli::command(), &mut io::stdout());
return Ok(ExitCode::SUCCESS);
}
// TODO(charlie): Included in `pyproject.toml`, but not inherited.
let fix = if configuration.fix {
// Construct the "default" settings. These are used when no `pyproject.toml`
// files are present, or files are injected from outside of the hierarchy.
let pyproject_strategy = resolve(cli.config, &overrides)?;
// Extract options that are included in `Settings`, but only apply at the top
// level.
let file_strategy = FileDiscovery {
respect_gitignore: match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.respect_gitignore,
PyprojectDiscovery::Hierarchical(settings) => settings.respect_gitignore,
},
};
let (fix, format) = match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => (settings.fix, settings.format),
PyprojectDiscovery::Hierarchical(settings) => (settings.fix, settings.format),
};
let autofix = if fix {
fixer::Mode::Apply
} else if matches!(configuration.format, SerializationFormat::Json) {
} else if matches!(format, SerializationFormat::Json) {
fixer::Mode::Generate
} else {
fixer::Mode::None
};
let format = configuration.format;
let settings = Settings::from_configuration(configuration, project_root.as_ref())?;
// Now that we've inferred the appropriate log level, add some debug
// information.
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
if let Some(code) = cli.explain {
commands::explain(&code, format)?;
commands::explain(&code, &format)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_settings {
commands::show_settings(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_files {
commands::show_files(&cli.files, &settings);
commands::show_files(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
@@ -272,7 +131,7 @@ fn inner_main() -> Result<ExitCode> {
let printer = Printer::new(&format, &log_level);
if cli.watch {
if matches!(fix, fixer::Mode::Generate | fixer::Mode::Apply) {
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
eprintln!("Warning: --fix is not enabled in watch mode.");
}
if cli.add_noqa {
@@ -289,7 +148,14 @@ fn inner_main() -> Result<ExitCode> {
printer.clear_screen()?;
printer.write_to_user("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, cache_enabled, &fixer::Mode::None);
let messages = commands::run(
&cli.files,
&pyproject_strategy,
&file_strategy,
&overrides,
cache_enabled,
&fixer::Mode::None,
)?;
printer.write_continuously(&messages)?;
// Configure the file watcher.
@@ -301,32 +167,40 @@ fn inner_main() -> Result<ExitCode> {
loop {
match rx.recv() {
Ok(e) => {
let paths = e?.paths;
let py_changed = paths.iter().any(|p| {
p.extension()
.map(|ext| ext.eq_ignore_ascii_case("py"))
Ok(event) => {
let paths = event?.paths;
let py_changed = paths.iter().any(|path| {
path.extension()
.map(|ext| ext == "py" || ext == "pyi")
.unwrap_or_default()
});
if py_changed {
printer.clear_screen()?;
printer.write_to_user("File change detected...\n");
let messages =
run_once(&cli.files, &settings, cache_enabled, &fixer::Mode::None);
let messages = commands::run(
&cli.files,
&pyproject_strategy,
&file_strategy,
&overrides,
cache_enabled,
&fixer::Mode::None,
)?;
printer.write_continuously(&messages)?;
}
}
Err(e) => return Err(e.into()),
Err(err) => return Err(err.into()),
}
}
} else if cli.add_noqa {
let modifications = add_noqa(&cli.files, &settings);
let modifications =
commands::add_noqa(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default {
println!("Added {modifications} noqa directives.");
}
} else if cli.autoformat {
let modifications = autoformat(&cli.files, &settings);
let modifications =
commands::autoformat(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default {
println!("Formatted {modifications} files.");
}
@@ -337,16 +211,23 @@ fn inner_main() -> Result<ExitCode> {
let diagnostics = if is_stdin {
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
let path = Path::new(&filename);
run_once_stdin(&settings, path, &fix)?
commands::run_stdin(&pyproject_strategy, path, &autofix)?
} else {
run_once(&cli.files, &settings, cache_enabled, &fix)
commands::run(
&cli.files,
&pyproject_strategy,
&file_strategy,
&overrides,
cache_enabled,
&autofix,
)?
};
// Always try to print violations (the printer itself may suppress output),
// unless we're writing fixes via stdin (in which case, the transformed
// source code goes to stdout).
if !(is_stdin && matches!(fix, fixer::Mode::Apply)) {
printer.write_once(&diagnostics, &fix)?;
if !(is_stdin && matches!(autofix, fixer::Mode::Apply)) {
printer.write_once(&diagnostics, &autofix)?;
}
// Check for updates if we're in a non-silent log level.

View File

@@ -1,7 +1,8 @@
use rustpython_ast::{ExcepthandlerKind, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::helpers::identifier_range;
use crate::checks::{Check, CheckKind};
use crate::source_code_locator::SourceCodeLocator;
fn get_complexity_number(stmts: &[Stmt]) -> usize {
let mut complexity = 0;
@@ -59,12 +60,13 @@ pub fn function_is_too_complex(
name: &str,
body: &[Stmt],
max_complexity: usize,
locator: &SourceCodeLocator,
) -> Option<Check> {
let complexity = get_complexity_number(body) + 1;
if complexity > max_complexity {
Some(Check::new(
CheckKind::FunctionIsTooComplex(name.to_string(), complexity),
Range::from_located(stmt),
identifier_range(stmt, locator),
))
} else {
None

View File

@@ -8,10 +8,10 @@ expression: checks
- 1
location:
row: 2
column: 0
column: 4
end_location:
row: 7
column: 0
row: 2
column: 11
fix: ~
- kind:
FunctionIsTooComplex:
@@ -19,10 +19,10 @@ expression: checks
- 1
location:
row: 7
column: 0
column: 4
end_location:
row: 12
column: 0
row: 7
column: 21
fix: ~
- kind:
FunctionIsTooComplex:
@@ -30,10 +30,10 @@ expression: checks
- 1
location:
row: 12
column: 0
column: 4
end_location:
row: 19
column: 0
row: 12
column: 14
fix: ~
- kind:
FunctionIsTooComplex:
@@ -41,10 +41,10 @@ expression: checks
- 3
location:
row: 19
column: 0
column: 4
end_location:
row: 29
column: 0
row: 19
column: 26
fix: ~
- kind:
FunctionIsTooComplex:
@@ -52,10 +52,10 @@ expression: checks
- 3
location:
row: 29
column: 0
column: 4
end_location:
row: 40
column: 0
row: 29
column: 14
fix: ~
- kind:
FunctionIsTooComplex:
@@ -63,10 +63,10 @@ expression: checks
- 2
location:
row: 40
column: 0
column: 4
end_location:
row: 46
column: 0
row: 40
column: 12
fix: ~
- kind:
FunctionIsTooComplex:
@@ -74,10 +74,10 @@ expression: checks
- 2
location:
row: 46
column: 0
column: 4
end_location:
row: 54
column: 0
row: 46
column: 12
fix: ~
- kind:
FunctionIsTooComplex:
@@ -85,10 +85,10 @@ expression: checks
- 2
location:
row: 54
column: 0
column: 4
end_location:
row: 62
column: 0
row: 54
column: 13
fix: ~
- kind:
FunctionIsTooComplex:
@@ -96,10 +96,10 @@ expression: checks
- 3
location:
row: 62
column: 0
column: 4
end_location:
row: 73
column: 0
row: 62
column: 20
fix: ~
- kind:
FunctionIsTooComplex:
@@ -107,10 +107,10 @@ expression: checks
- 2
location:
row: 63
column: 4
column: 8
end_location:
row: 69
column: 4
row: 63
column: 9
fix: ~
- kind:
FunctionIsTooComplex:
@@ -118,10 +118,10 @@ expression: checks
- 1
location:
row: 64
column: 8
column: 12
end_location:
row: 67
column: 8
row: 64
column: 13
fix: ~
- kind:
FunctionIsTooComplex:
@@ -129,10 +129,10 @@ expression: checks
- 4
location:
row: 73
column: 0
column: 4
end_location:
row: 85
column: 0
row: 73
column: 12
fix: ~
- kind:
FunctionIsTooComplex:
@@ -140,10 +140,10 @@ expression: checks
- 3
location:
row: 85
column: 0
column: 4
end_location:
row: 96
column: 0
row: 85
column: 22
fix: ~
- kind:
FunctionIsTooComplex:
@@ -151,10 +151,10 @@ expression: checks
- 3
location:
row: 96
column: 0
column: 10
end_location:
row: 107
column: 0
row: 96
column: 16
fix: ~
- kind:
FunctionIsTooComplex:
@@ -162,10 +162,10 @@ expression: checks
- 1
location:
row: 107
column: 0
column: 4
end_location:
row: 112
column: 0
row: 107
column: 20
fix: ~
- kind:
FunctionIsTooComplex:
@@ -173,10 +173,10 @@ expression: checks
- 9
location:
row: 113
column: 4
column: 8
end_location:
row: 139
column: 0
row: 113
column: 14
fix: ~
- kind:
FunctionIsTooComplex:
@@ -184,10 +184,10 @@ expression: checks
- 1
location:
row: 118
column: 12
column: 16
end_location:
row: 121
column: 12
row: 118
column: 17
fix: ~
- kind:
FunctionIsTooComplex:
@@ -195,10 +195,10 @@ expression: checks
- 2
location:
row: 121
column: 12
column: 16
end_location:
row: 125
column: 8
row: 121
column: 17
fix: ~
- kind:
FunctionIsTooComplex:
@@ -206,10 +206,10 @@ expression: checks
- 1
location:
row: 126
column: 12
column: 16
end_location:
row: 129
column: 12
row: 126
column: 17
fix: ~
- kind:
FunctionIsTooComplex:
@@ -217,10 +217,10 @@ expression: checks
- 1
location:
row: 129
column: 12
column: 16
end_location:
row: 132
column: 12
row: 129
column: 21
fix: ~
- kind:
FunctionIsTooComplex:
@@ -228,10 +228,10 @@ expression: checks
- 1
location:
row: 132
column: 12
column: 16
end_location:
row: 135
column: 12
row: 132
column: 20
fix: ~
- kind:
FunctionIsTooComplex:
@@ -239,9 +239,9 @@ expression: checks
- 1
location:
row: 135
column: 12
column: 16
end_location:
row: 138
column: 8
row: 135
column: 25
fix: ~

View File

@@ -8,10 +8,10 @@ expression: checks
- 4
location:
row: 73
column: 0
column: 4
end_location:
row: 85
column: 0
row: 73
column: 12
fix: ~
- kind:
FunctionIsTooComplex:
@@ -19,9 +19,9 @@ expression: checks
- 9
location:
row: 113
column: 4
column: 8
end_location:
row: 139
column: 0
row: 113
column: 14
fix: ~

61
src/pandas_vet/checks.rs Normal file
View File

@@ -0,0 +1,61 @@
use rustpython_ast::{Constant, Expr, ExprKind, Keyword};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
/// PDV002
pub fn inplace_argument(keywords: &[Keyword]) -> Option<Check> {
for keyword in keywords {
let arg = keyword.node.arg.as_ref()?;
if arg == "inplace" {
let is_true_literal = match &keyword.node.value.node {
ExprKind::Constant {
value: Constant::Bool(boolean),
..
} => *boolean,
_ => false,
};
if is_true_literal {
return Some(Check::new(
CheckKind::UseOfInplaceArgument,
Range::from_located(keyword),
));
}
}
}
None
}
/// PDV015
pub fn use_of_pd_merge(func: &Expr) -> Option<Check> {
if let ExprKind::Attribute { attr, value, .. } = &func.node {
if let ExprKind::Name { id, .. } = &value.node {
if id == "pd" && attr == "merge" {
return Some(Check::new(
CheckKind::UseOfPdMerge,
Range::from_located(func),
));
}
}
}
None
}
/// PDV901
pub fn assignment_to_df(targets: &[Expr]) -> Option<Check> {
if targets.len() != 1 {
return None;
}
let target = &targets[0];
let ExprKind::Name { id, .. } = &target.node else {
return None;
};
if id != "df" {
return None;
}
Some(Check::new(
CheckKind::DfIsABadVariableName,
Range::from_located(target),
))
}

109
src/pandas_vet/mod.rs Normal file
View File

@@ -0,0 +1,109 @@
pub mod checks;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use rustpython_parser::lexer::LexResult;
use test_case::test_case;
use textwrap::dedent;
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::linter::check_path;
use crate::source_code_locator::SourceCodeLocator;
use crate::{directives, rustpython_helpers, settings};
fn check_code(contents: &str, expected: &[CheckCode]) -> Result<()> {
let contents = dedent(contents);
let settings = settings::Settings::for_rules(CheckCodePrefix::PDV.codes());
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let directives = directives::extract_directives(
&tokens,
&locator,
directives::Flags::from_settings(&settings),
);
let mut checks = check_path(
Path::new("<filename>"),
&contents,
tokens,
&locator,
&directives,
&settings,
true,
false,
)?;
checks.sort_by_key(|check| check.location);
let actual = checks
.iter()
.map(|check| check.kind.code().clone())
.collect::<Vec<_>>();
assert_eq!(actual, expected);
Ok(())
}
#[test_case("df.drop(['a'], axis=1, inplace=False)", &[]; "PDV002_pass")]
#[test_case("df.drop(['a'], axis=1, inplace=True)", &[CheckCode::PDV002]; "PDV002_fail")]
#[test_case("nas = pd.isna(val)", &[]; "PDV003_pass")]
#[test_case("nulls = pd.isnull(val)", &[CheckCode::PDV003]; "PDV003_fail")]
#[test_case("print('bah humbug')", &[]; "PDV003_allows_other_calls")]
#[test_case("not_nas = pd.notna(val)", &[]; "PDV004_pass")]
#[test_case("not_nulls = pd.notnull(val)", &[CheckCode::PDV004]; "PDV004_fail")]
#[test_case("new_df = df.loc['d':, 'A':'C']", &[]; "PDV007_pass_loc")]
#[test_case("new_df = df.iloc[[1, 3, 5], [1, 3]]", &[]; "PDV007_pass_iloc")]
#[test_case("s = df.ix[[0, 2], 'A']", &[CheckCode::PDV007]; "PDV007_fail")]
#[test_case("index = df.loc[:, ['B', 'A']]", &[]; "PDV008_pass")]
#[test_case("index = df.at[:, ['B', 'A']]", &[CheckCode::PDV008]; "PDV008_fail")]
#[test_case("index = df.iloc[:, 1:3]", &[]; "PDV009_pass")]
#[test_case("index = df.iat[:, 1:3]", &[CheckCode::PDV009]; "PDV009_fail")]
#[test_case(r#"table = df.pivot_table(
df,
values='D',
index=['A', 'B'],
columns=['C'],
aggfunc=np.sum,
fill_value=0
)
"#, &[]; "PDV010_pass")]
#[test_case(r#"table = pd.pivot(
df,
index='foo',
columns='bar',
values='baz'
)
"#, &[CheckCode::PDV010]; "PDV010_fail_pivot")]
#[test_case("result = df.to_array()", &[]; "PDV011_pass_to_array")]
#[test_case("result = df.array", &[]; "PDV011_pass_array")]
#[test_case("result = df.values", &[CheckCode::PDV011]; "PDV011_fail_values")]
// TODO: Check that the attribute access is NOT a method call
// #[test_case("result = {}.values()", &[]; "PDV011_pass_values_call")]
#[test_case("result = values", &[]; "PDV011_pass_node_name")]
#[test_case("employees = pd.read_csv(input_file)", &[]; "PDV012_pass_read_csv")]
#[test_case("employees = pd.read_table(input_file)", &[CheckCode::PDV012]; "PDV012_fail_read_table")]
#[test_case("employees = read_table", &[]; "PDV012_node_Name_pass")]
#[test_case(r#"table = df.melt(
id_vars='airline',
value_vars=['ATL', 'DEN', 'DFW'],
value_name='airline delay'
)
"#, &[]; "PDV013_pass")]
#[test_case("table = df.stack(level=-1, dropna=True)", &[CheckCode::PDV013]; "PDV013_fail_stack")]
#[test_case("df1.merge(df2)", &[]; "PD015_pass_merge_on_dataframe")]
#[test_case("df1.merge(df2, 'inner')", &[]; "PD015_pass_merge_on_dataframe_with_multiple_args")]
#[test_case("pd.merge(df1, df2)", &[CheckCode::PDV015]; "PD015_fail_merge_on_pandas_object")]
#[test_case(
"pd.to_datetime(timestamp * 10 ** 9).strftime('%Y-%m-%d %H:%M:%S.%f')",
&[];
"PD015_pass_other_pd_function"
)]
#[test_case("employees = pd.DataFrame(employee_dict)", &[]; "PDV901_pass_non_df")]
#[test_case("employees_df = pd.DataFrame(employee_dict)", &[]; "PDV901_pass_part_df")]
#[test_case("my_function(df=data)", &[]; "PDV901_pass_df_param")]
#[test_case("df = pd.DataFrame()", &[CheckCode::PDV901]; "PDV901_fail_df_var")]
fn test_pandas_vet(code: &str, expected: &[CheckCode]) -> Result<()> {
check_code(code, expected)?;
Ok(())
}
}

View File

@@ -1,47 +1,53 @@
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use rustpython_ast::{Arg, Arguments, Expr, ExprKind, Stmt};
use crate::ast::function_type;
use crate::ast::helpers::identifier_range;
use crate::ast::types::{Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pep8_naming::helpers;
use crate::pep8_naming::settings::Settings;
use crate::python::string::{self};
use crate::source_code_locator::SourceCodeLocator;
/// N801
pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option<Check> {
pub fn invalid_class_name(
class_def: &Stmt,
name: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
let stripped = name.strip_prefix('_').unwrap_or(name);
if !stripped.chars().next().map_or(false, char::is_uppercase) || stripped.contains('_') {
return Some(Check::new(
CheckKind::InvalidClassName(name.to_string()),
Range::from_located(class_def),
identifier_range(class_def, locator),
));
}
None
}
/// N802
pub fn invalid_function_name(func_def: &Stmt, name: &str, settings: &Settings) -> Option<Check> {
if name.to_lowercase() != name
&& !settings
.ignore_names
.iter()
.any(|ignore_name| ignore_name == name)
{
pub fn invalid_function_name(
func_def: &Stmt,
name: &str,
ignore_names: &[String],
locator: &SourceCodeLocator,
) -> Option<Check> {
if name.to_lowercase() != name && !ignore_names.iter().any(|ignore_name| ignore_name == name) {
return Some(Check::new(
CheckKind::InvalidFunctionName(name.to_string()),
Range::from_located(func_def),
identifier_range(func_def, locator),
));
}
None
}
/// N803
pub fn invalid_argument_name(name: &str, location: Range) -> Option<Check> {
pub fn invalid_argument_name(name: &str, arg: &Arg) -> Option<Check> {
if name.to_lowercase() != name {
return Some(Check::new(
CheckKind::InvalidArgumentName(name.to_string()),
location,
Range::from_located(arg),
));
}
None
@@ -124,7 +130,12 @@ pub fn invalid_first_argument_name_for_method(
}
/// N807
pub fn dunder_function_name(scope: &Scope, stmt: &Stmt, name: &str) -> Option<Check> {
pub fn dunder_function_name(
scope: &Scope,
stmt: &Stmt,
name: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if matches!(scope.kind, ScopeKind::Class(_)) {
return None;
}
@@ -138,7 +149,7 @@ pub fn dunder_function_name(scope: &Scope, stmt: &Stmt, name: &str) -> Option<Ch
Some(Check::new(
CheckKind::DunderFunctionName,
Range::from_located(stmt),
identifier_range(stmt, locator),
))
}
@@ -147,11 +158,12 @@ pub fn constant_imported_as_non_constant(
import_from: &Stmt,
name: &str,
asname: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if string::is_upper(name) && !string::is_upper(asname) {
return Some(Check::new(
CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
identifier_range(import_from, locator),
));
}
None
@@ -162,11 +174,12 @@ pub fn lowercase_imported_as_non_lowercase(
import_from: &Stmt,
name: &str,
asname: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if !string::is_upper(name) && string::is_lower(name) && asname.to_lowercase() != asname {
return Some(Check::new(
CheckKind::LowercaseImportedAsNonLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
identifier_range(import_from, locator),
));
}
None
@@ -177,11 +190,12 @@ pub fn camelcase_imported_as_lowercase(
import_from: &Stmt,
name: &str,
asname: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if helpers::is_camelcase(name) && string::is_lower(asname) {
return Some(Check::new(
CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
identifier_range(import_from, locator),
));
}
None
@@ -192,6 +206,7 @@ pub fn camelcase_imported_as_constant(
import_from: &Stmt,
name: &str,
asname: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if helpers::is_camelcase(name)
&& !string::is_lower(asname)
@@ -200,7 +215,7 @@ pub fn camelcase_imported_as_constant(
{
return Some(Check::new(
CheckKind::CamelcaseImportedAsConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
identifier_range(import_from, locator),
));
}
None
@@ -211,6 +226,7 @@ pub fn camelcase_imported_as_acronym(
import_from: &Stmt,
name: &str,
asname: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if helpers::is_camelcase(name)
&& !string::is_lower(asname)
@@ -219,7 +235,7 @@ pub fn camelcase_imported_as_acronym(
{
return Some(Check::new(
CheckKind::CamelcaseImportedAsAcronym(name.to_string(), asname.to_string()),
Range::from_located(import_from),
identifier_range(import_from, locator),
));
}
None

View File

@@ -6,45 +6,45 @@ expression: checks
InvalidClassName: bad
location:
row: 1
column: 0
column: 6
end_location:
row: 5
column: 0
row: 1
column: 9
fix: ~
- kind:
InvalidClassName: _bad
location:
row: 5
column: 0
column: 6
end_location:
row: 9
column: 0
row: 5
column: 10
fix: ~
- kind:
InvalidClassName: bad_class
location:
row: 9
column: 0
column: 6
end_location:
row: 13
column: 0
row: 9
column: 15
fix: ~
- kind:
InvalidClassName: Bad_Class
location:
row: 13
column: 0
column: 6
end_location:
row: 17
column: 0
row: 13
column: 15
fix: ~
- kind:
InvalidClassName: BAD_CLASS
location:
row: 17
column: 0
column: 6
end_location:
row: 21
column: 0
row: 17
column: 15
fix: ~

View File

@@ -6,45 +6,45 @@ expression: checks
InvalidFunctionName: Bad
location:
row: 4
column: 0
column: 4
end_location:
row: 8
column: 0
row: 4
column: 7
fix: ~
- kind:
InvalidFunctionName: _Bad
location:
row: 8
column: 0
column: 4
end_location:
row: 12
column: 0
row: 8
column: 8
fix: ~
- kind:
InvalidFunctionName: BAD
location:
row: 12
column: 0
column: 4
end_location:
row: 16
column: 0
row: 12
column: 7
fix: ~
- kind:
InvalidFunctionName: BAD_FUNC
location:
row: 16
column: 0
column: 4
end_location:
row: 20
column: 0
row: 16
column: 12
fix: ~
- kind:
InvalidFunctionName: testTest
location:
row: 40
column: 4
column: 8
end_location:
row: 42
column: 0
row: 40
column: 16
fix: ~

View File

@@ -5,17 +5,17 @@ expression: checks
- kind: DunderFunctionName
location:
row: 1
column: 0
column: 4
end_location:
row: 5
column: 0
row: 1
column: 11
fix: ~
- kind: DunderFunctionName
location:
row: 14
column: 4
column: 8
end_location:
row: 17
column: 4
row: 14
column: 15
fix: ~

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