Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9f56ee76e | ||
|
|
b4bfa87104 | ||
|
|
b9f42bf5e5 | ||
|
|
8281d414ca | ||
|
|
7e45a9f2e2 | ||
|
|
a000cd4a09 | ||
|
|
d8b4b92733 | ||
|
|
27de342e75 | ||
|
|
d805067683 | ||
|
|
1ea2e93f8e | ||
|
|
dc180dc277 | ||
|
|
6be910ae07 | ||
|
|
ba85eb846c | ||
|
|
d067efe265 | ||
|
|
549ea2f85f | ||
|
|
d814ebd21f | ||
|
|
3f272b6cf8 | ||
|
|
76891a8c07 | ||
|
|
e389201b5f | ||
|
|
4b2020d03a | ||
|
|
0aa356c96c | ||
|
|
630b4b627d |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.180
|
||||
rev: v0.0.183
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# 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))
|
||||
|
||||
37
Cargo.lock
generated
37
Cargo.lock
generated
@@ -724,7 +724,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.180-dev.0"
|
||||
version = "0.0.183-dev.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.29",
|
||||
@@ -894,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"
|
||||
@@ -1827,7 +1845,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.180"
|
||||
version = "0.0.183"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -1849,6 +1867,7 @@ dependencies = [
|
||||
"getrandom 0.2.8",
|
||||
"glob",
|
||||
"globset",
|
||||
"ignore",
|
||||
"insta",
|
||||
"itertools",
|
||||
"libcst",
|
||||
@@ -1876,12 +1895,13 @@ dependencies = [
|
||||
"titlecase",
|
||||
"toml",
|
||||
"update-informer",
|
||||
"ureq",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.180"
|
||||
version = "0.0.183"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.29",
|
||||
@@ -1899,7 +1919,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_macros"
|
||||
version = "0.0.180"
|
||||
version = "0.0.183"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2304,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"
|
||||
|
||||
@@ -6,7 +6,7 @@ members = [
|
||||
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.180"
|
||||
version = "0.0.183"
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
@@ -30,6 +30,7 @@ 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" }
|
||||
@@ -42,7 +43,7 @@ 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.180", 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 = "8d879a53197f9c73062f6160410bdba796a71cbf" }
|
||||
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "8d879a53197f9c73062f6160410bdba796a71cbf" }
|
||||
@@ -70,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"]
|
||||
|
||||
106
README.md
106
README.md
@@ -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.180
|
||||
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>
|
||||
@@ -341,15 +345,18 @@ directory hierarchy is used for every individual file, with all paths in the `py
|
||||
|
||||
There are a few exceptions to these rules:
|
||||
|
||||
1. If a configuration file is passed directly via `--config`, those settings are used for across
|
||||
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_.
|
||||
2. 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_.
|
||||
3. Any `pyproject.toml`-supported settings that are provided on the command-line (e.g., via
|
||||
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),
|
||||
@@ -365,6 +372,18 @@ extend = "../pyproject.toml"
|
||||
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
|
||||
@@ -600,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)
|
||||
|
||||
@@ -765,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 |
|
||||
@@ -842,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.
|
||||
@@ -1707,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
|
||||
@@ -1920,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)
|
||||
|
||||
4
flake8_to_ruff/Cargo.lock
generated
4
flake8_to_ruff/Cargo.lock
generated
@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8_to_ruff"
|
||||
version = "0.0.180"
|
||||
version = "0.0.183"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1975,7 +1975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.180"
|
||||
version = "0.0.183"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.180-dev.0"
|
||||
version = "0.0.183-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -258,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,
|
||||
@@ -269,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,
|
||||
@@ -304,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,
|
||||
@@ -315,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,
|
||||
@@ -350,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,
|
||||
@@ -361,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,
|
||||
@@ -396,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,
|
||||
@@ -407,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,
|
||||
@@ -442,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,
|
||||
@@ -453,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,
|
||||
@@ -496,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,
|
||||
@@ -543,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,
|
||||
@@ -578,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,
|
||||
@@ -590,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,
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
19
resources/test/fixtures/flake8_errmsg/EM.py
vendored
Normal file
19
resources/test/fixtures/flake8_errmsg/EM.py
vendored
Normal 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)
|
||||
@@ -4,3 +4,10 @@ import os
|
||||
if True:
|
||||
x = 1; import sys
|
||||
import os
|
||||
|
||||
if True:
|
||||
x = 1; \
|
||||
import os
|
||||
|
||||
x = 1; \
|
||||
import os
|
||||
|
||||
39
resources/test/fixtures/pycodestyle/constant_literals.py
vendored
Normal file
39
resources/test/fixtures/pycodestyle/constant_literals.py
vendored
Normal 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
|
||||
16
resources/test/fixtures/pyflakes/F401_7.py
vendored
Normal file
16
resources/test/fixtures/pyflakes/F401_7.py
vendored
Normal 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,
|
||||
)
|
||||
1
resources/test/fixtures/pyflakes/F811_20.py
vendored
1
resources/test/fixtures/pyflakes/F811_20.py
vendored
@@ -7,7 +7,6 @@ import fu
|
||||
|
||||
|
||||
class bar:
|
||||
# STOPSHIP: This errors.
|
||||
fu = 1
|
||||
|
||||
|
||||
|
||||
51
resources/test/fixtures/pyflakes/multi_statement_lines.py
vendored
Normal file
51
resources/test/fixtures/pyflakes/multi_statement_lines.py
vendored
Normal 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
|
||||
3
resources/test/fixtures/pyproject.toml
vendored
3
resources/test/fixtures/pyproject.toml
vendored
@@ -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"
|
||||
|
||||
|
||||
87
resources/test/fixtures/pyupgrade/UP016.py
vendored
Normal file
87
resources/test/fixtures/pyupgrade/UP016.py
vendored
Normal 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
1
resources/test/project/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
examples/generated
|
||||
@@ -9,33 +9,37 @@ Running from the repo root should pick up and enforce the appropriate settings f
|
||||
|
||||
```
|
||||
∴ cargo run resources/test/project/
|
||||
Found 5 error(s).
|
||||
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/file.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
3 potentially fixable with the --fix option.
|
||||
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 5 error(s).
|
||||
∴ (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/file.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
3 potentially fixable with the --fix option.
|
||||
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 .
|
||||
∴ (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
|
||||
@@ -46,28 +50,40 @@ docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
file paths from the current working directory:
|
||||
|
||||
```
|
||||
∴ cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/
|
||||
Found 9 error(s).
|
||||
∴ (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/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
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/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
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/file.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
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.
|
||||
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 3 error(s).
|
||||
∴ (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.
|
||||
```
|
||||
|
||||
2
resources/test/project/examples/.dotfiles/script.py
Executable file
2
resources/test/project/examples/.dotfiles/script.py
Executable file
@@ -0,0 +1,2 @@
|
||||
import numpy as np
|
||||
from app import app_file
|
||||
@@ -1,6 +1,7 @@
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
src = ["."]
|
||||
extend-select = ["I001"]
|
||||
# Enable I001, and re-enable F841, to test extension priority.
|
||||
extend-select = ["I001", "F841"]
|
||||
extend-ignore = ["F401"]
|
||||
extend-exclude = ["./docs/concepts/file.py"]
|
||||
|
||||
5
resources/test/project/examples/excluded/script.py
Executable file
5
resources/test/project/examples/excluded/script.py
Executable file
@@ -0,0 +1,5 @@
|
||||
import os
|
||||
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
@@ -1,3 +1,5 @@
|
||||
[tool.ruff]
|
||||
src = [".", "python_modules/*"]
|
||||
exclude = ["examples/excluded"]
|
||||
extend-select = ["I001"]
|
||||
extend-ignore = ["F841"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.180"
|
||||
version = "0.0.183"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_macros"
|
||||
version = "0.0.180"
|
||||
version = "0.0.183"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,4 @@ impl Fix {
|
||||
end_location: at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dummy(location: Location) -> Self {
|
||||
Self {
|
||||
content: String::new(),
|
||||
location,
|
||||
end_location: location,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
261
src/check_ast.rs
261
src/check_ast.rs
@@ -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();
|
||||
|
||||
|
||||
150
src/checks.rs
150
src/checks.rs
@@ -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,
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
13
src/cli.rs
13
src/cli.rs
@@ -86,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 the settings Ruff used for the first matching file.
|
||||
/// 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.
|
||||
@@ -156,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,
|
||||
@@ -212,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>,
|
||||
|
||||
@@ -3,6 +3,7 @@ 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"))]
|
||||
@@ -17,20 +18,22 @@ 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::Strategy;
|
||||
use crate::resolver::{FileDiscovery, PyprojectDiscovery};
|
||||
use crate::settings::types::SerializationFormat;
|
||||
|
||||
/// Run the linter over a collection of files.
|
||||
pub fn run(
|
||||
files: &[PathBuf],
|
||||
strategy: &Strategy,
|
||||
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::resolve_python_files(files, strategy, overrides)?;
|
||||
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);
|
||||
|
||||
@@ -40,19 +43,23 @@ pub fn run(
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
let path = entry.path();
|
||||
let settings = resolver.resolve(path, strategy);
|
||||
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((
|
||||
e.path().map(Path::to_owned),
|
||||
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, strategy);
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
if settings.enabled.contains(&CheckCode::E902) {
|
||||
Diagnostics::new(vec![Message {
|
||||
kind: CheckKind::IOError(message),
|
||||
@@ -93,14 +100,14 @@ fn read_from_stdin() -> Result<String> {
|
||||
|
||||
/// Run the linter over a single file, read from `stdin`.
|
||||
pub fn run_stdin(
|
||||
strategy: &Strategy,
|
||||
strategy: &PyprojectDiscovery,
|
||||
filename: &Path,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Result<Diagnostics> {
|
||||
let stdin = read_from_stdin()?;
|
||||
let settings = match strategy {
|
||||
Strategy::Fixed(settings) => settings,
|
||||
Strategy::Hierarchical(settings) => settings,
|
||||
PyprojectDiscovery::Fixed(settings) => settings,
|
||||
PyprojectDiscovery::Hierarchical(settings) => settings,
|
||||
};
|
||||
let mut diagnostics = lint_stdin(filename, &stdin, settings, autofix)?;
|
||||
diagnostics.messages.sort_unstable();
|
||||
@@ -108,10 +115,16 @@ pub fn run_stdin(
|
||||
}
|
||||
|
||||
/// Add `noqa` directives to a collection of files.
|
||||
pub fn add_noqa(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<usize> {
|
||||
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::resolve_python_files(files, strategy, overrides)?;
|
||||
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);
|
||||
|
||||
@@ -120,7 +133,7 @@ pub fn add_noqa(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
let settings = resolver.resolve(path, strategy);
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
match add_noqa_to_path(path, settings) {
|
||||
Ok(count) => Some(count),
|
||||
Err(e) => {
|
||||
@@ -138,10 +151,16 @@ pub fn add_noqa(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -
|
||||
}
|
||||
|
||||
/// Automatically format a collection of files.
|
||||
pub fn autoformat(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<usize> {
|
||||
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::resolve_python_files(files, strategy, overrides)?;
|
||||
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);
|
||||
|
||||
@@ -150,7 +169,7 @@ pub fn autoformat(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides)
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
let settings = resolver.resolve(path, strategy);
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
match autoformat_path(path, settings) {
|
||||
Ok(()) => Some(()),
|
||||
Err(e) => {
|
||||
@@ -168,9 +187,15 @@ pub fn autoformat(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides)
|
||||
}
|
||||
|
||||
/// Print the user-facing configuration settings.
|
||||
pub fn show_settings(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<()> {
|
||||
pub fn show_settings(
|
||||
files: &[PathBuf],
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
file_strategy: &FileDiscovery,
|
||||
overrides: &Overrides,
|
||||
) -> Result<()> {
|
||||
// Collect all files in the hierarchy.
|
||||
let (paths, resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
|
||||
let (paths, resolver) =
|
||||
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
||||
|
||||
// Print the list of files.
|
||||
let Some(entry) = paths
|
||||
@@ -180,7 +205,7 @@ pub fn show_settings(files: &[PathBuf], strategy: &Strategy, overrides: &Overrid
|
||||
bail!("No files found under the given path");
|
||||
};
|
||||
let path = entry.path();
|
||||
let settings = resolver.resolve(path, strategy);
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
println!("Resolved settings for: {path:?}");
|
||||
println!("{settings:#?}");
|
||||
|
||||
@@ -188,9 +213,15 @@ pub fn show_settings(files: &[PathBuf], strategy: &Strategy, overrides: &Overrid
|
||||
}
|
||||
|
||||
/// Show the list of files to be checked based on current settings.
|
||||
pub fn show_files(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<()> {
|
||||
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::resolve_python_files(files, strategy, overrides)?;
|
||||
let (paths, _resolver) =
|
||||
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
||||
|
||||
// Print the list of files.
|
||||
for entry in paths
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
46
src/flake8_errmsg/checks.rs
Normal file
46
src/flake8_errmsg/checks.rs
Normal 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
50
src/flake8_errmsg/mod.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
32
src/flake8_errmsg/settings.rs
Normal file
32
src/flake8_errmsg/settings.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: ~
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
|
||||
@@ -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"
|
||||
location:
|
||||
row: 5
|
||||
column: 0
|
||||
end_location:
|
||||
row: 7
|
||||
column: 0
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -50,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;
|
||||
@@ -66,6 +67,7 @@ pub mod logging;
|
||||
pub mod mccabe;
|
||||
pub mod message;
|
||||
mod noqa;
|
||||
mod pandas_vet;
|
||||
pub mod pep8_naming;
|
||||
pub mod printer;
|
||||
mod pycodestyle;
|
||||
@@ -87,7 +89,7 @@ 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) {
|
||||
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() {
|
||||
|
||||
@@ -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(
|
||||
|
||||
57
src/main.rs
57
src/main.rs
@@ -20,7 +20,7 @@ use ::ruff::autofix::fixer;
|
||||
use ::ruff::cli::{extract_log_level, Cli};
|
||||
use ::ruff::logging::{set_up_logging, LogLevel};
|
||||
use ::ruff::printer::Printer;
|
||||
use ::ruff::resolver::Strategy;
|
||||
use ::ruff::resolver::PyprojectDiscovery;
|
||||
use ::ruff::settings::configuration::Configuration;
|
||||
use ::ruff::settings::types::SerializationFormat;
|
||||
use ::ruff::settings::{pyproject, Settings};
|
||||
@@ -33,31 +33,31 @@ use colored::Colorize;
|
||||
use notify::{recommended_watcher, RecursiveMode, Watcher};
|
||||
use path_absolutize::path_dedot;
|
||||
use ruff::cli::Overrides;
|
||||
use ruff::resolver::{resolve_settings, Relativity};
|
||||
use ruff::resolver::{resolve_settings, FileDiscovery, Relativity};
|
||||
|
||||
/// Resolve the relevant settings strategy and defaults for the current
|
||||
/// invocation.
|
||||
fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<Strategy> {
|
||||
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(Strategy::Fixed(settings))
|
||||
} else if let Some(pyproject) = pyproject::find_pyproject_toml(path_dedot::CWD.as_path()) {
|
||||
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(Strategy::Hierarchical(settings))
|
||||
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(Strategy::Hierarchical(settings))
|
||||
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
|
||||
@@ -67,7 +67,7 @@ fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<Strategy> {
|
||||
// Apply command-line options that override defaults.
|
||||
config.apply(overrides.clone());
|
||||
let settings = Settings::from_configuration(config, &path_dedot::CWD)?;
|
||||
Ok(Strategy::Hierarchical(settings))
|
||||
Ok(PyprojectDiscovery::Hierarchical(settings))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,13 +87,19 @@ fn inner_main() -> Result<ExitCode> {
|
||||
|
||||
// Construct the "default" settings. These are used when no `pyproject.toml`
|
||||
// files are present, or files are injected from outside of the hierarchy.
|
||||
let strategy = resolve(cli.config, &overrides)?;
|
||||
let pyproject_strategy = resolve(cli.config, &overrides)?;
|
||||
|
||||
// Extract options that are included in `Settings`, but only apply at the top
|
||||
// level.
|
||||
let (fix, format) = match &strategy {
|
||||
Strategy::Fixed(settings) => (settings.fix, settings.format),
|
||||
Strategy::Hierarchical(settings) => (settings.fix, settings.format),
|
||||
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
|
||||
@@ -108,11 +114,11 @@ fn inner_main() -> Result<ExitCode> {
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
if cli.show_settings {
|
||||
commands::show_settings(&cli.files, &strategy, &overrides)?;
|
||||
commands::show_settings(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
if cli.show_files {
|
||||
commands::show_files(&cli.files, &strategy, &overrides)?;
|
||||
commands::show_files(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
@@ -144,7 +150,8 @@ fn inner_main() -> Result<ExitCode> {
|
||||
|
||||
let messages = commands::run(
|
||||
&cli.files,
|
||||
&strategy,
|
||||
&pyproject_strategy,
|
||||
&file_strategy,
|
||||
&overrides,
|
||||
cache_enabled,
|
||||
&fixer::Mode::None,
|
||||
@@ -173,7 +180,8 @@ fn inner_main() -> Result<ExitCode> {
|
||||
|
||||
let messages = commands::run(
|
||||
&cli.files,
|
||||
&strategy,
|
||||
&pyproject_strategy,
|
||||
&file_strategy,
|
||||
&overrides,
|
||||
cache_enabled,
|
||||
&fixer::Mode::None,
|
||||
@@ -185,12 +193,14 @@ fn inner_main() -> Result<ExitCode> {
|
||||
}
|
||||
}
|
||||
} else if cli.add_noqa {
|
||||
let modifications = commands::add_noqa(&cli.files, &strategy, &overrides)?;
|
||||
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 = commands::autoformat(&cli.files, &strategy, &overrides)?;
|
||||
let modifications =
|
||||
commands::autoformat(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
|
||||
if modifications > 0 && log_level >= LogLevel::Default {
|
||||
println!("Formatted {modifications} files.");
|
||||
}
|
||||
@@ -201,9 +211,16 @@ 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);
|
||||
commands::run_stdin(&strategy, path, &autofix)?
|
||||
commands::run_stdin(&pyproject_strategy, path, &autofix)?
|
||||
} else {
|
||||
commands::run(&cli.files, &strategy, &overrides, cache_enabled, &autofix)?
|
||||
commands::run(
|
||||
&cli.files,
|
||||
&pyproject_strategy,
|
||||
&file_strategy,
|
||||
&overrides,
|
||||
cache_enabled,
|
||||
&autofix,
|
||||
)?
|
||||
};
|
||||
|
||||
// Always try to print violations (the printer itself may suppress output),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,10 +8,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 2
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 3
|
||||
column: 8
|
||||
row: 2
|
||||
column: 11
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -19,10 +19,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 7
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 8
|
||||
column: 10
|
||||
row: 7
|
||||
column: 21
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -30,10 +30,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 12
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 15
|
||||
column: 12
|
||||
row: 12
|
||||
column: 14
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -41,10 +41,10 @@ expression: checks
|
||||
- 3
|
||||
location:
|
||||
row: 19
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 25
|
||||
column: 47
|
||||
row: 19
|
||||
column: 26
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -52,10 +52,10 @@ expression: checks
|
||||
- 3
|
||||
location:
|
||||
row: 29
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 36
|
||||
column: 47
|
||||
row: 29
|
||||
column: 14
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -63,10 +63,10 @@ expression: checks
|
||||
- 2
|
||||
location:
|
||||
row: 40
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 42
|
||||
column: 16
|
||||
row: 40
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -74,10 +74,10 @@ expression: checks
|
||||
- 2
|
||||
location:
|
||||
row: 46
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 50
|
||||
column: 19
|
||||
row: 46
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -85,10 +85,10 @@ expression: checks
|
||||
- 2
|
||||
location:
|
||||
row: 54
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 58
|
||||
column: 16
|
||||
row: 54
|
||||
column: 13
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -96,10 +96,10 @@ expression: checks
|
||||
- 3
|
||||
location:
|
||||
row: 62
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 69
|
||||
column: 7
|
||||
row: 62
|
||||
column: 20
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -107,10 +107,10 @@ expression: checks
|
||||
- 2
|
||||
location:
|
||||
row: 63
|
||||
column: 4
|
||||
column: 8
|
||||
end_location:
|
||||
row: 67
|
||||
column: 11
|
||||
row: 63
|
||||
column: 9
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -118,10 +118,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 64
|
||||
column: 8
|
||||
column: 12
|
||||
end_location:
|
||||
row: 65
|
||||
column: 16
|
||||
row: 64
|
||||
column: 13
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -129,10 +129,10 @@ expression: checks
|
||||
- 4
|
||||
location:
|
||||
row: 73
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 81
|
||||
column: 16
|
||||
row: 73
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -140,10 +140,10 @@ expression: checks
|
||||
- 3
|
||||
location:
|
||||
row: 85
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 92
|
||||
column: 16
|
||||
row: 85
|
||||
column: 22
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -151,10 +151,10 @@ expression: checks
|
||||
- 3
|
||||
location:
|
||||
row: 96
|
||||
column: 0
|
||||
column: 10
|
||||
end_location:
|
||||
row: 103
|
||||
column: 12
|
||||
row: 96
|
||||
column: 16
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -162,10 +162,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 107
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 108
|
||||
column: 17
|
||||
row: 107
|
||||
column: 20
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -173,10 +173,10 @@ expression: checks
|
||||
- 9
|
||||
location:
|
||||
row: 113
|
||||
column: 4
|
||||
column: 8
|
||||
end_location:
|
||||
row: 138
|
||||
column: 40
|
||||
row: 113
|
||||
column: 14
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -184,10 +184,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 118
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 119
|
||||
column: 20
|
||||
row: 118
|
||||
column: 17
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -195,10 +195,10 @@ expression: checks
|
||||
- 2
|
||||
location:
|
||||
row: 121
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 123
|
||||
column: 24
|
||||
row: 121
|
||||
column: 17
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -206,10 +206,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 126
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 127
|
||||
column: 20
|
||||
row: 126
|
||||
column: 17
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -217,10 +217,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 129
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 130
|
||||
column: 20
|
||||
row: 129
|
||||
column: 21
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -228,9 +228,9 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 132
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 133
|
||||
row: 132
|
||||
column: 20
|
||||
fix: ~
|
||||
- kind:
|
||||
@@ -239,9 +239,9 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 135
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 136
|
||||
column: 20
|
||||
row: 135
|
||||
column: 25
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ expression: checks
|
||||
- 4
|
||||
location:
|
||||
row: 73
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 81
|
||||
column: 16
|
||||
row: 73
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -19,9 +19,9 @@ expression: checks
|
||||
- 9
|
||||
location:
|
||||
row: 113
|
||||
column: 4
|
||||
column: 8
|
||||
end_location:
|
||||
row: 138
|
||||
column: 40
|
||||
row: 113
|
||||
column: 14
|
||||
fix: ~
|
||||
|
||||
|
||||
61
src/pandas_vet/checks.rs
Normal file
61
src/pandas_vet/checks.rs
Normal 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
109
src/pandas_vet/mod.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -6,45 +6,45 @@ expression: checks
|
||||
InvalidClassName: bad
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
column: 6
|
||||
end_location:
|
||||
row: 2
|
||||
column: 8
|
||||
row: 1
|
||||
column: 9
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidClassName: _bad
|
||||
location:
|
||||
row: 5
|
||||
column: 0
|
||||
column: 6
|
||||
end_location:
|
||||
row: 6
|
||||
column: 8
|
||||
row: 5
|
||||
column: 10
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidClassName: bad_class
|
||||
location:
|
||||
row: 9
|
||||
column: 0
|
||||
column: 6
|
||||
end_location:
|
||||
row: 10
|
||||
column: 8
|
||||
row: 9
|
||||
column: 15
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidClassName: Bad_Class
|
||||
location:
|
||||
row: 13
|
||||
column: 0
|
||||
column: 6
|
||||
end_location:
|
||||
row: 14
|
||||
column: 8
|
||||
row: 13
|
||||
column: 15
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidClassName: BAD_CLASS
|
||||
location:
|
||||
row: 17
|
||||
column: 0
|
||||
column: 6
|
||||
end_location:
|
||||
row: 18
|
||||
column: 8
|
||||
row: 17
|
||||
column: 15
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -6,45 +6,45 @@ expression: checks
|
||||
InvalidFunctionName: Bad
|
||||
location:
|
||||
row: 4
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 5
|
||||
column: 8
|
||||
row: 4
|
||||
column: 7
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidFunctionName: _Bad
|
||||
location:
|
||||
row: 8
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 9
|
||||
row: 8
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidFunctionName: BAD
|
||||
location:
|
||||
row: 12
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 13
|
||||
column: 8
|
||||
row: 12
|
||||
column: 7
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidFunctionName: BAD_FUNC
|
||||
location:
|
||||
row: 16
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 17
|
||||
column: 8
|
||||
row: 16
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidFunctionName: testTest
|
||||
location:
|
||||
row: 40
|
||||
column: 4
|
||||
column: 8
|
||||
end_location:
|
||||
row: 41
|
||||
column: 19
|
||||
row: 40
|
||||
column: 16
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -5,17 +5,17 @@ expression: checks
|
||||
- kind: DunderFunctionName
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 2
|
||||
column: 8
|
||||
row: 1
|
||||
column: 11
|
||||
fix: ~
|
||||
- kind: DunderFunctionName
|
||||
location:
|
||||
row: 14
|
||||
column: 4
|
||||
column: 8
|
||||
end_location:
|
||||
row: 15
|
||||
column: 12
|
||||
row: 14
|
||||
column: 15
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use itertools::izip;
|
||||
use rustpython_ast::{Location, Stmt, StmtKind};
|
||||
use rustpython_ast::{Located, Location, Stmt, StmtKind};
|
||||
use rustpython_parser::ast::{Cmpop, Expr, ExprKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
@@ -65,11 +65,11 @@ fn is_ambiguous_name(name: &str) -> bool {
|
||||
}
|
||||
|
||||
/// E741
|
||||
pub fn ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
|
||||
pub fn ambiguous_variable_name<T>(name: &str, located: &Located<T>) -> Option<Check> {
|
||||
if is_ambiguous_name(name) {
|
||||
Some(Check::new(
|
||||
CheckKind::AmbiguousVariableName(name.to_string()),
|
||||
location,
|
||||
Range::from_located(located),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -44,4 +44,16 @@ mod tests {
|
||||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_literals() -> Result<()> {
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/pycodestyle/constant_literals.py"),
|
||||
&settings::Settings::for_rules(vec![CheckCode::E711, CheckCode::E712, CheckCode::F632]),
|
||||
true,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use rustc_hash::FxHashMap;
|
||||
use rustpython_ast::{Arguments, Location, StmtKind};
|
||||
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Stmt, Unaryop};
|
||||
|
||||
use crate::ast::helpers;
|
||||
use crate::ast::helpers::{match_leading_content, match_trailing_content};
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::whitespace::leading_space;
|
||||
@@ -46,9 +47,10 @@ pub fn literal_comparisons(
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
let op = ops.first().unwrap();
|
||||
let comparator = left;
|
||||
|
||||
// Check `left`.
|
||||
let mut comparator = left;
|
||||
let next = &comparators[0];
|
||||
if check_none_comparisons
|
||||
&& matches!(
|
||||
comparator.node,
|
||||
@@ -59,24 +61,21 @@ pub fn literal_comparisons(
|
||||
)
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
let mut check = Check::new(
|
||||
let check = Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
Range::from_located(comparator),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
// Dummy replacement
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
if checker.patch(check.kind.code()) && !helpers::is_constant_non_singleton(next) {
|
||||
bad_ops.insert(0, Cmpop::Is);
|
||||
}
|
||||
checks.push(check);
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
let mut check = Check::new(
|
||||
let check = Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::NotEq),
|
||||
Range::from_located(comparator),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
if checker.patch(check.kind.code()) && !helpers::is_constant_non_singleton(next) {
|
||||
bad_ops.insert(0, Cmpop::IsNot);
|
||||
}
|
||||
checks.push(check);
|
||||
@@ -90,23 +89,21 @@ pub fn literal_comparisons(
|
||||
} = comparator.node
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
let mut check = Check::new(
|
||||
let check = Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
|
||||
Range::from_located(comparator),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
if checker.patch(check.kind.code()) && !helpers::is_constant_non_singleton(next) {
|
||||
bad_ops.insert(0, Cmpop::Is);
|
||||
}
|
||||
checks.push(check);
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
let mut check = Check::new(
|
||||
let check = Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
|
||||
Range::from_located(comparator),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
if checker.patch(check.kind.code()) && !helpers::is_constant_non_singleton(next) {
|
||||
bad_ops.insert(0, Cmpop::IsNot);
|
||||
}
|
||||
checks.push(check);
|
||||
@@ -115,10 +112,10 @@ pub fn literal_comparisons(
|
||||
}
|
||||
|
||||
// Check each comparator in order.
|
||||
for (idx, (op, comparator)) in izip!(ops, comparators).enumerate() {
|
||||
for (idx, (op, next)) in izip!(ops, comparators).enumerate() {
|
||||
if check_none_comparisons
|
||||
&& matches!(
|
||||
comparator.node,
|
||||
next.node,
|
||||
ExprKind::Constant {
|
||||
value: Constant::None,
|
||||
kind: None
|
||||
@@ -126,23 +123,25 @@ pub fn literal_comparisons(
|
||||
)
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
let mut check = Check::new(
|
||||
let check = Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
Range::from_located(comparator),
|
||||
Range::from_located(next),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
if checker.patch(check.kind.code())
|
||||
&& !helpers::is_constant_non_singleton(comparator)
|
||||
{
|
||||
bad_ops.insert(idx, Cmpop::Is);
|
||||
}
|
||||
checks.push(check);
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
let mut check = Check::new(
|
||||
let check = Check::new(
|
||||
CheckKind::NoneComparison(RejectedCmpop::NotEq),
|
||||
Range::from_located(comparator),
|
||||
Range::from_located(next),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
if checker.patch(check.kind.code())
|
||||
&& !helpers::is_constant_non_singleton(comparator)
|
||||
{
|
||||
bad_ops.insert(idx, Cmpop::IsNot);
|
||||
}
|
||||
checks.push(check);
|
||||
@@ -153,34 +152,40 @@ pub fn literal_comparisons(
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Bool(value),
|
||||
kind: None,
|
||||
} = comparator.node
|
||||
} = next.node
|
||||
{
|
||||
if matches!(op, Cmpop::Eq) {
|
||||
let mut check = Check::new(
|
||||
let check = Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
|
||||
Range::from_located(comparator),
|
||||
Range::from_located(next),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
if checker.patch(check.kind.code())
|
||||
&& !helpers::is_constant_non_singleton(comparator)
|
||||
{
|
||||
bad_ops.insert(idx, Cmpop::Is);
|
||||
}
|
||||
checks.push(check);
|
||||
}
|
||||
if matches!(op, Cmpop::NotEq) {
|
||||
let mut check = Check::new(
|
||||
let check = Check::new(
|
||||
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
|
||||
Range::from_located(comparator),
|
||||
Range::from_located(next),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
if checker.patch(check.kind.code())
|
||||
&& !helpers::is_constant_non_singleton(comparator)
|
||||
{
|
||||
bad_ops.insert(idx, Cmpop::IsNot);
|
||||
}
|
||||
checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
comparator = next;
|
||||
}
|
||||
|
||||
// TODO(charlie): Respect `noqa` directives. If one of the operators has a
|
||||
// `noqa`, but another doesn't, both will be removed here.
|
||||
if !bad_ops.is_empty() {
|
||||
// Replace the entire comparison expression.
|
||||
let ops = ops
|
||||
@@ -190,9 +195,9 @@ pub fn literal_comparisons(
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(content) = compare(left, &ops, comparators) {
|
||||
if let Some(check) = checks.last_mut() {
|
||||
check.fix = Some(Fix::replacement(
|
||||
content,
|
||||
for check in &mut checks {
|
||||
check.amend(Fix::replacement(
|
||||
content.to_string(),
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
));
|
||||
|
||||
@@ -139,13 +139,13 @@ expression: checks
|
||||
row: 26
|
||||
column: 12
|
||||
fix:
|
||||
content: ""
|
||||
content: x is None is not None
|
||||
location:
|
||||
row: 26
|
||||
column: 3
|
||||
end_location:
|
||||
row: 26
|
||||
column: 3
|
||||
column: 20
|
||||
- kind:
|
||||
NoneComparison: NotEq
|
||||
location:
|
||||
|
||||
@@ -175,13 +175,13 @@ expression: checks
|
||||
row: 25
|
||||
column: 14
|
||||
fix:
|
||||
content: ""
|
||||
content: res is True is not False
|
||||
location:
|
||||
row: 25
|
||||
column: 3
|
||||
end_location:
|
||||
row: 25
|
||||
column: 3
|
||||
column: 23
|
||||
- kind:
|
||||
TrueFalseComparison:
|
||||
- false
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
---
|
||||
source: src/pycodestyle/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: IsLiteral
|
||||
location:
|
||||
row: 4
|
||||
column: 3
|
||||
end_location:
|
||||
row: 4
|
||||
column: 17
|
||||
fix:
|
||||
content: "=="
|
||||
location:
|
||||
row: 4
|
||||
column: 9
|
||||
end_location:
|
||||
row: 4
|
||||
column: 11
|
||||
- kind: IsLiteral
|
||||
location:
|
||||
row: 6
|
||||
column: 3
|
||||
end_location:
|
||||
row: 6
|
||||
column: 16
|
||||
fix:
|
||||
content: "=="
|
||||
location:
|
||||
row: 6
|
||||
column: 9
|
||||
end_location:
|
||||
row: 6
|
||||
column: 11
|
||||
- kind: IsLiteral
|
||||
location:
|
||||
row: 8
|
||||
column: 3
|
||||
end_location:
|
||||
row: 8
|
||||
column: 16
|
||||
fix:
|
||||
content: "=="
|
||||
location:
|
||||
row: 8
|
||||
column: 8
|
||||
end_location:
|
||||
row: 8
|
||||
column: 10
|
||||
- kind: IsLiteral
|
||||
location:
|
||||
row: 10
|
||||
column: 3
|
||||
end_location:
|
||||
row: 10
|
||||
column: 17
|
||||
fix:
|
||||
content: "=="
|
||||
location:
|
||||
row: 10
|
||||
column: 9
|
||||
end_location:
|
||||
row: 10
|
||||
column: 11
|
||||
- kind: IsLiteral
|
||||
location:
|
||||
row: 12
|
||||
column: 3
|
||||
end_location:
|
||||
row: 12
|
||||
column: 17
|
||||
fix:
|
||||
content: "=="
|
||||
location:
|
||||
row: 12
|
||||
column: 9
|
||||
end_location:
|
||||
row: 12
|
||||
column: 11
|
||||
- kind:
|
||||
TrueFalseComparison:
|
||||
- false
|
||||
- Eq
|
||||
location:
|
||||
row: 14
|
||||
column: 3
|
||||
end_location:
|
||||
row: 14
|
||||
column: 8
|
||||
fix:
|
||||
content: False is None
|
||||
location:
|
||||
row: 14
|
||||
column: 3
|
||||
end_location:
|
||||
row: 14
|
||||
column: 16
|
||||
- kind:
|
||||
NoneComparison: Eq
|
||||
location:
|
||||
row: 14
|
||||
column: 12
|
||||
end_location:
|
||||
row: 14
|
||||
column: 16
|
||||
fix:
|
||||
content: False is None
|
||||
location:
|
||||
row: 14
|
||||
column: 3
|
||||
end_location:
|
||||
row: 14
|
||||
column: 16
|
||||
- kind:
|
||||
NoneComparison: Eq
|
||||
location:
|
||||
row: 16
|
||||
column: 3
|
||||
end_location:
|
||||
row: 16
|
||||
column: 7
|
||||
fix:
|
||||
content: None is False
|
||||
location:
|
||||
row: 16
|
||||
column: 3
|
||||
end_location:
|
||||
row: 16
|
||||
column: 16
|
||||
- kind:
|
||||
TrueFalseComparison:
|
||||
- false
|
||||
- Eq
|
||||
location:
|
||||
row: 16
|
||||
column: 11
|
||||
end_location:
|
||||
row: 16
|
||||
column: 16
|
||||
fix:
|
||||
content: None is False
|
||||
location:
|
||||
row: 16
|
||||
column: 3
|
||||
end_location:
|
||||
row: 16
|
||||
column: 16
|
||||
- kind:
|
||||
NoneComparison: Eq
|
||||
location:
|
||||
row: 22
|
||||
column: 12
|
||||
end_location:
|
||||
row: 22
|
||||
column: 16
|
||||
fix: ~
|
||||
- kind:
|
||||
NoneComparison: Eq
|
||||
location:
|
||||
row: 24
|
||||
column: 3
|
||||
end_location:
|
||||
row: 24
|
||||
column: 7
|
||||
fix: ~
|
||||
- kind:
|
||||
TrueFalseComparison:
|
||||
- false
|
||||
- Eq
|
||||
location:
|
||||
row: 26
|
||||
column: 12
|
||||
end_location:
|
||||
row: 26
|
||||
column: 17
|
||||
fix: ~
|
||||
- kind:
|
||||
TrueFalseComparison:
|
||||
- false
|
||||
- Eq
|
||||
location:
|
||||
row: 28
|
||||
column: 3
|
||||
end_location:
|
||||
row: 28
|
||||
column: 8
|
||||
fix: ~
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{bail, Result};
|
||||
use libcst_native::{
|
||||
Call, Codegen, CodegenState, Dict, DictElement, Expression, ImportNames, SmallStatement,
|
||||
Statement,
|
||||
Call, Codegen, CodegenState, Dict, DictElement, Expression, ImportNames,
|
||||
ParenthesizableWhitespace, SmallStatement, Statement,
|
||||
};
|
||||
use rustpython_ast::{Expr, Stmt};
|
||||
|
||||
@@ -14,11 +14,11 @@ use crate::source_code_locator::SourceCodeLocator;
|
||||
|
||||
/// Generate a `Fix` to remove any unused imports from an `import` statement.
|
||||
pub fn remove_unused_imports(
|
||||
locator: &SourceCodeLocator,
|
||||
unused_imports: &Vec<(&String, &Range)>,
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
deleted: &[&Stmt],
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Result<Fix> {
|
||||
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
|
||||
let mut tree = match_module(&module_text)?;
|
||||
@@ -60,12 +60,25 @@ pub fn remove_unused_imports(
|
||||
}
|
||||
}
|
||||
|
||||
// But avoid destroying any trailing comments.
|
||||
if let Some(alias) = aliases.last_mut() {
|
||||
alias.comma = trailing_comma;
|
||||
let has_comment = if let Some(comma) = &alias.comma {
|
||||
match &comma.whitespace_after {
|
||||
ParenthesizableWhitespace::SimpleWhitespace(_) => false,
|
||||
ParenthesizableWhitespace::ParenthesizedWhitespace(whitespace) => {
|
||||
whitespace.first_line.comment.is_some()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !has_comment {
|
||||
alias.comma = trailing_comma;
|
||||
}
|
||||
}
|
||||
|
||||
if aliases.is_empty() {
|
||||
helpers::remove_stmt(stmt, parent, deleted)
|
||||
helpers::delete_stmt(stmt, parent, deleted, locator)
|
||||
} else {
|
||||
let mut state = CodegenState::default();
|
||||
tree.codegen(&mut state);
|
||||
@@ -80,9 +93,9 @@ pub fn remove_unused_imports(
|
||||
|
||||
/// Generate a `Fix` to remove unused keys from format dict.
|
||||
pub fn remove_unused_format_arguments_from_dict(
|
||||
locator: &SourceCodeLocator,
|
||||
unused_arguments: &[&str],
|
||||
stmt: &Expr,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Result<Fix> {
|
||||
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
|
||||
let mut tree = match_module(&module_text)?;
|
||||
@@ -126,9 +139,9 @@ pub fn remove_unused_format_arguments_from_dict(
|
||||
|
||||
/// Generate a `Fix` to remove unused keyword arguments from format call.
|
||||
pub fn remove_unused_keyword_arguments_from_format_call(
|
||||
locator: &SourceCodeLocator,
|
||||
unused_arguments: &[&str],
|
||||
location: Range,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Result<Fix> {
|
||||
let module_text = locator.slice_source_code_range(&location);
|
||||
let mut tree = match_module(&module_text)?;
|
||||
|
||||
@@ -28,6 +28,7 @@ mod tests {
|
||||
#[test_case(CheckCode::F401, Path::new("F401_4.py"); "F401_4")]
|
||||
#[test_case(CheckCode::F401, Path::new("F401_5.py"); "F401_5")]
|
||||
#[test_case(CheckCode::F401, Path::new("F401_6.py"); "F401_6")]
|
||||
#[test_case(CheckCode::F401, Path::new("F401_7.py"); "F401_7")]
|
||||
#[test_case(CheckCode::F402, Path::new("F402.py"); "F402")]
|
||||
#[test_case(CheckCode::F403, Path::new("F403.py"); "F403")]
|
||||
#[test_case(CheckCode::F404, Path::new("F404.py"); "F404")]
|
||||
@@ -154,6 +155,18 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_statement_lines() -> Result<()> {
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/pyflakes/multi_statement_lines.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F401),
|
||||
true,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A re-implementation of the Pyflakes test runner.
|
||||
/// Note that all tests marked with `#[ignore]` should be considered TODOs.
|
||||
fn flakes(contents: &str, expected: &[CheckCode]) -> Result<()> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use itertools::izip;
|
||||
use once_cell::unsync::Lazy;
|
||||
use rustpython_ast::{Cmpop, Constant, Expr, ExprKind};
|
||||
use rustpython_ast::{Cmpop, Expr};
|
||||
|
||||
use crate::ast::helpers;
|
||||
use crate::ast::operations::locate_cmpops;
|
||||
@@ -9,28 +9,6 @@ use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
fn is_singleton(expr: &Expr) -> bool {
|
||||
matches!(
|
||||
expr.node,
|
||||
ExprKind::Constant {
|
||||
value: Constant::None | Constant::Bool(_) | Constant::Ellipsis,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn is_constant(expr: &Expr) -> bool {
|
||||
match &expr.node {
|
||||
ExprKind::Constant { .. } => true,
|
||||
ExprKind::Tuple { elts, .. } => elts.iter().all(is_constant),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_constant_non_singleton(expr: &Expr) -> bool {
|
||||
is_constant(expr) && !is_singleton(expr)
|
||||
}
|
||||
|
||||
/// F632
|
||||
pub fn invalid_literal_comparison(
|
||||
checker: &mut Checker,
|
||||
@@ -43,7 +21,8 @@ pub fn invalid_literal_comparison(
|
||||
let mut left = left;
|
||||
for (index, (op, right)) in izip!(ops, comparators).enumerate() {
|
||||
if matches!(op, Cmpop::Is | Cmpop::IsNot)
|
||||
&& (is_constant_non_singleton(left) || is_constant_non_singleton(right))
|
||||
&& (helpers::is_constant_non_singleton(left)
|
||||
|| helpers::is_constant_non_singleton(right))
|
||||
{
|
||||
let mut check = Check::new(CheckKind::IsLiteral, location);
|
||||
if checker.patch(check.kind.code()) {
|
||||
|
||||
@@ -115,7 +115,7 @@ pub(crate) fn percent_format_extra_named_arguments(
|
||||
location,
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
if let Ok(fix) = remove_unused_format_arguments_from_dict(checker.locator, &missing, right)
|
||||
if let Ok(fix) = remove_unused_format_arguments_from_dict(&missing, right, checker.locator)
|
||||
{
|
||||
check.amend(fix);
|
||||
}
|
||||
@@ -273,7 +273,7 @@ pub(crate) fn string_dot_format_extra_named_arguments(
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
if let Ok(fix) =
|
||||
remove_unused_keyword_arguments_from_format_call(checker.locator, &missing, location)
|
||||
remove_unused_keyword_arguments_from_format_call(&missing, location, checker.locator)
|
||||
{
|
||||
check.amend(fix);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
source: src/pyflakes/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- module.B
|
||||
- false
|
||||
location:
|
||||
row: 5
|
||||
column: 4
|
||||
end_location:
|
||||
row: 5
|
||||
column: 5
|
||||
fix:
|
||||
content: "from module import (\n A, # noqa: F401\n )"
|
||||
location:
|
||||
row: 3
|
||||
column: 0
|
||||
end_location:
|
||||
row: 6
|
||||
column: 1
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- module.A
|
||||
- false
|
||||
location:
|
||||
row: 14
|
||||
column: 4
|
||||
end_location:
|
||||
row: 14
|
||||
column: 5
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 13
|
||||
column: 0
|
||||
end_location:
|
||||
row: 17
|
||||
column: 0
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- module.B
|
||||
- false
|
||||
location:
|
||||
row: 15
|
||||
column: 4
|
||||
end_location:
|
||||
row: 15
|
||||
column: 5
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 13
|
||||
column: 0
|
||||
end_location:
|
||||
row: 17
|
||||
column: 0
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
---
|
||||
source: src/pyflakes/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 3
|
||||
column: 11
|
||||
end_location:
|
||||
row: 3
|
||||
column: 14
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 3
|
||||
column: 4
|
||||
end_location:
|
||||
row: 3
|
||||
column: 16
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 4
|
||||
column: 11
|
||||
end_location:
|
||||
row: 4
|
||||
column: 14
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 4
|
||||
column: 4
|
||||
end_location:
|
||||
row: 4
|
||||
column: 20
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 7
|
||||
column: 11
|
||||
end_location:
|
||||
row: 7
|
||||
column: 14
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 7
|
||||
column: 4
|
||||
end_location:
|
||||
row: 8
|
||||
column: 0
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 11
|
||||
column: 11
|
||||
end_location:
|
||||
row: 11
|
||||
column: 14
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 11
|
||||
column: 4
|
||||
end_location:
|
||||
row: 12
|
||||
column: 10
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 16
|
||||
column: 18
|
||||
end_location:
|
||||
row: 16
|
||||
column: 21
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 16
|
||||
column: 11
|
||||
end_location:
|
||||
row: 16
|
||||
column: 21
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 21
|
||||
column: 16
|
||||
end_location:
|
||||
row: 21
|
||||
column: 19
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 21
|
||||
column: 9
|
||||
end_location:
|
||||
row: 21
|
||||
column: 19
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 26
|
||||
column: 17
|
||||
end_location:
|
||||
row: 26
|
||||
column: 20
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 26
|
||||
column: 10
|
||||
end_location:
|
||||
row: 26
|
||||
column: 20
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 30
|
||||
column: 18
|
||||
end_location:
|
||||
row: 30
|
||||
column: 21
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 30
|
||||
column: 11
|
||||
end_location:
|
||||
row: 30
|
||||
column: 23
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 31
|
||||
column: 22
|
||||
end_location:
|
||||
row: 31
|
||||
column: 25
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 31
|
||||
column: 15
|
||||
end_location:
|
||||
row: 31
|
||||
column: 31
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 35
|
||||
column: 15
|
||||
end_location:
|
||||
row: 35
|
||||
column: 18
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 35
|
||||
column: 8
|
||||
end_location:
|
||||
row: 36
|
||||
column: 4
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 40
|
||||
column: 16
|
||||
end_location:
|
||||
row: 40
|
||||
column: 19
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 40
|
||||
column: 9
|
||||
end_location:
|
||||
row: 41
|
||||
column: 9
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 46
|
||||
column: 7
|
||||
end_location:
|
||||
row: 46
|
||||
column: 10
|
||||
fix:
|
||||
content: ""
|
||||
location:
|
||||
row: 46
|
||||
column: 0
|
||||
end_location:
|
||||
row: 46
|
||||
column: 10
|
||||
- kind:
|
||||
UnusedImport:
|
||||
- foo
|
||||
- false
|
||||
location:
|
||||
row: 51
|
||||
column: 7
|
||||
end_location:
|
||||
row: 51
|
||||
column: 10
|
||||
fix:
|
||||
content: "\n"
|
||||
location:
|
||||
row: 51
|
||||
column: 0
|
||||
end_location:
|
||||
row: 51
|
||||
column: 10
|
||||
|
||||
@@ -170,12 +170,25 @@ pub fn remove_unnecessary_future_import(
|
||||
aliases.remove(*index);
|
||||
}
|
||||
|
||||
// But avoid destroying any trailing comments.
|
||||
if let Some(alias) = aliases.last_mut() {
|
||||
alias.comma = trailing_comma;
|
||||
let has_comment = if let Some(comma) = &alias.comma {
|
||||
match &comma.whitespace_after {
|
||||
ParenthesizableWhitespace::SimpleWhitespace(_) => false,
|
||||
ParenthesizableWhitespace::ParenthesizedWhitespace(whitespace) => {
|
||||
whitespace.first_line.comment.is_some()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !has_comment {
|
||||
alias.comma = trailing_comma;
|
||||
}
|
||||
}
|
||||
|
||||
if aliases.is_empty() {
|
||||
autofix::helpers::remove_stmt(stmt, parent, deleted)
|
||||
autofix::helpers::delete_stmt(stmt, parent, deleted, locator)
|
||||
} else {
|
||||
let mut state = CodegenState::default();
|
||||
tree.codegen(&mut state);
|
||||
|
||||
@@ -36,6 +36,7 @@ mod tests {
|
||||
#[test_case(CheckCode::UP013, Path::new("UP013.py"); "UP013")]
|
||||
#[test_case(CheckCode::UP014, Path::new("UP014.py"); "UP014")]
|
||||
#[test_case(CheckCode::UP015, Path::new("UP015.py"); "UP015")]
|
||||
#[test_case(CheckCode::UP016, Path::new("UP016.py"); "UP016")]
|
||||
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
|
||||
let mut checks = test_path(
|
||||
|
||||
@@ -2,6 +2,7 @@ pub use convert_named_tuple_functional_to_class::convert_named_tuple_functional_
|
||||
pub use convert_typed_dict_functional_to_class::convert_typed_dict_functional_to_class;
|
||||
pub use deprecated_unittest_alias::deprecated_unittest_alias;
|
||||
pub use redundant_open_modes::redundant_open_modes;
|
||||
pub use remove_six_compat::remove_six_compat;
|
||||
pub use super_call_with_parameters::super_call_with_parameters;
|
||||
pub use type_of_primitive::type_of_primitive;
|
||||
pub use unnecessary_encode_utf8::unnecessary_encode_utf8;
|
||||
@@ -16,6 +17,7 @@ mod convert_named_tuple_functional_to_class;
|
||||
mod convert_typed_dict_functional_to_class;
|
||||
mod deprecated_unittest_alias;
|
||||
mod redundant_open_modes;
|
||||
mod remove_six_compat;
|
||||
mod super_call_with_parameters;
|
||||
mod type_of_primitive;
|
||||
mod unnecessary_encode_utf8;
|
||||
|
||||
427
src/pyupgrade/plugins/remove_six_compat.rs
Normal file
427
src/pyupgrade/plugins/remove_six_compat.rs
Normal file
@@ -0,0 +1,427 @@
|
||||
use anyhow::{bail, Result};
|
||||
use log::error;
|
||||
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Keyword, StmtKind};
|
||||
|
||||
use crate::ast::helpers::{collect_call_paths, create_expr, create_stmt, dealias_call_path};
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::code_gen::SourceGenerator;
|
||||
use crate::SourceCodeLocator;
|
||||
|
||||
/// Return `true` if the `Expr` is a reference to `${module}.${any}`.
|
||||
fn is_module_member(call_path: &[&str], module: &str) -> bool {
|
||||
call_path
|
||||
.first()
|
||||
.map_or(false, |module_name| *module_name == module)
|
||||
}
|
||||
|
||||
fn map_name(name: &str, expr: &Expr, patch: bool) -> Option<Check> {
|
||||
let replacement = match name {
|
||||
"text_type" => Some("str"),
|
||||
"binary_type" => Some("bytes"),
|
||||
"class_types" => Some("(type,)"),
|
||||
"string_types" => Some("(str,)"),
|
||||
"integer_types" => Some("(int,)"),
|
||||
"unichr" => Some("chr"),
|
||||
"iterbytes" => Some("iter"),
|
||||
"print_" => Some("print"),
|
||||
"exec_" => Some("exec"),
|
||||
"advance_iterator" => Some("next"),
|
||||
"next" => Some("next"),
|
||||
"range" => Some("range"), // TODO: six.moves
|
||||
"xrange" => Some("range"), // TODO: six.moves
|
||||
"callable" => Some("callable"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(replacement) = replacement {
|
||||
let mut check = Check::new(CheckKind::RemoveSixCompat, Range::from_located(expr));
|
||||
if patch {
|
||||
check.amend(Fix::replacement(
|
||||
replacement.to_string(),
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
));
|
||||
}
|
||||
Some(check)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_by_str_literal(
|
||||
arg: &Expr,
|
||||
binary: bool,
|
||||
expr: &Expr,
|
||||
patch: bool,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Check> {
|
||||
match &arg.node {
|
||||
ExprKind::Constant { .. } => {
|
||||
let mut check = Check::new(CheckKind::RemoveSixCompat, Range::from_located(expr));
|
||||
if patch {
|
||||
let content = format!(
|
||||
"{}{}",
|
||||
if binary { "b" } else { "" },
|
||||
locator.slice_source_code_range(&Range {
|
||||
location: arg.location,
|
||||
end_location: arg.end_location.unwrap(),
|
||||
})
|
||||
);
|
||||
check.amend(Fix::replacement(
|
||||
content,
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
));
|
||||
};
|
||||
Some(check)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// `func(arg)` => `arg.attr`
|
||||
fn replace_call_on_arg_by_arg_attribute(
|
||||
attr: &str,
|
||||
arg: &Expr,
|
||||
expr: &Expr,
|
||||
patch: bool,
|
||||
) -> Result<Check> {
|
||||
let attribute = ExprKind::Attribute {
|
||||
value: Box::new(arg.clone()),
|
||||
attr: attr.to_string(),
|
||||
ctx: ExprContext::Load,
|
||||
};
|
||||
replace_by_expr_kind(attribute, expr, patch)
|
||||
}
|
||||
|
||||
// `func(arg, **args)` => `arg.method(**args)`
|
||||
fn replace_call_on_arg_by_arg_method_call(
|
||||
method_name: &str,
|
||||
args: &[Expr],
|
||||
expr: &Expr,
|
||||
patch: bool,
|
||||
) -> Result<Option<Check>> {
|
||||
if args.is_empty() {
|
||||
bail!("Expected at least one argument");
|
||||
}
|
||||
if let ([arg], other_args) = args.split_at(1) {
|
||||
let call = ExprKind::Call {
|
||||
func: Box::new(create_expr(ExprKind::Attribute {
|
||||
value: Box::new(arg.clone()),
|
||||
attr: method_name.to_string(),
|
||||
ctx: ExprContext::Load,
|
||||
})),
|
||||
args: other_args
|
||||
.iter()
|
||||
.map(|arg| create_expr(arg.node.clone()))
|
||||
.collect(),
|
||||
keywords: vec![],
|
||||
};
|
||||
let expr = replace_by_expr_kind(call, expr, patch)?;
|
||||
Ok(Some(expr))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// `expr` => `Expr(expr_kind)`
|
||||
fn replace_by_expr_kind(node: ExprKind, expr: &Expr, patch: bool) -> Result<Check> {
|
||||
let mut check = Check::new(CheckKind::RemoveSixCompat, Range::from_located(expr));
|
||||
if patch {
|
||||
let mut generator = SourceGenerator::new();
|
||||
generator.unparse_expr(&create_expr(node), 0);
|
||||
let content = generator.generate()?;
|
||||
check.amend(Fix::replacement(
|
||||
content,
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
));
|
||||
}
|
||||
Ok(check)
|
||||
}
|
||||
|
||||
fn replace_by_stmt_kind(node: StmtKind, expr: &Expr, patch: bool) -> Result<Check> {
|
||||
let mut check = Check::new(CheckKind::RemoveSixCompat, Range::from_located(expr));
|
||||
if patch {
|
||||
let mut generator = SourceGenerator::new();
|
||||
generator.unparse_stmt(&create_stmt(node));
|
||||
let content = generator.generate()?;
|
||||
check.amend(Fix::replacement(
|
||||
content,
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
));
|
||||
}
|
||||
Ok(check)
|
||||
}
|
||||
|
||||
// => `raise exc from cause`
|
||||
fn replace_by_raise_from(
|
||||
exc: Option<ExprKind>,
|
||||
cause: Option<ExprKind>,
|
||||
expr: &Expr,
|
||||
patch: bool,
|
||||
) -> Result<Check> {
|
||||
let stmt_kind = StmtKind::Raise {
|
||||
exc: exc.map(|exc| Box::new(create_expr(exc))),
|
||||
cause: cause.map(|cause| Box::new(create_expr(cause))),
|
||||
};
|
||||
replace_by_stmt_kind(stmt_kind, expr, patch)
|
||||
}
|
||||
|
||||
fn replace_by_index_on_arg(
|
||||
arg: &Expr,
|
||||
index: &ExprKind,
|
||||
expr: &Expr,
|
||||
patch: bool,
|
||||
) -> Result<Check> {
|
||||
let index = ExprKind::Subscript {
|
||||
value: Box::new(create_expr(arg.node.clone())),
|
||||
slice: Box::new(create_expr(index.clone())),
|
||||
ctx: ExprContext::Load,
|
||||
};
|
||||
replace_by_expr_kind(index, expr, patch)
|
||||
}
|
||||
|
||||
fn handle_reraise(args: &[Expr], expr: &Expr, patch: bool) -> Result<Option<Check>> {
|
||||
if let [_, exc, tb] = args {
|
||||
let check = replace_by_raise_from(
|
||||
Some(ExprKind::Call {
|
||||
func: Box::new(create_expr(ExprKind::Attribute {
|
||||
value: Box::new(create_expr(exc.node.clone())),
|
||||
attr: "with_traceback".to_string(),
|
||||
ctx: ExprContext::Load,
|
||||
})),
|
||||
args: vec![create_expr(tb.node.clone())],
|
||||
keywords: vec![],
|
||||
}),
|
||||
None,
|
||||
expr,
|
||||
patch,
|
||||
)?;
|
||||
Ok(Some(check))
|
||||
} else if let [arg] = args {
|
||||
if let ExprKind::Starred { value, .. } = &arg.node {
|
||||
if let ExprKind::Call { func, .. } = &value.node {
|
||||
if let ExprKind::Attribute { value, attr, .. } = &func.node {
|
||||
if let ExprKind::Name { id, .. } = &value.node {
|
||||
if id == "sys" && attr == "exc_info" {
|
||||
let check = replace_by_raise_from(None, None, expr, patch)?;
|
||||
return Ok(Some(check));
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_func(
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
expr: &Expr,
|
||||
patch: bool,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Result<Option<Check>> {
|
||||
let func_name = match &func.node {
|
||||
ExprKind::Attribute { attr, .. } => attr,
|
||||
ExprKind::Name { id, .. } => id,
|
||||
_ => bail!("Unexpected func: {:?}", func),
|
||||
};
|
||||
let check = match (func_name.as_str(), args, keywords) {
|
||||
("b", [arg], []) => replace_by_str_literal(arg, true, expr, patch, locator),
|
||||
("ensure_binary", [arg], []) => replace_by_str_literal(arg, true, expr, patch, locator),
|
||||
("u", [arg], []) => replace_by_str_literal(arg, false, expr, patch, locator),
|
||||
("ensure_str", [arg], []) => replace_by_str_literal(arg, false, expr, patch, locator),
|
||||
("ensure_text", [arg], []) => replace_by_str_literal(arg, false, expr, patch, locator),
|
||||
("iteritems", args, []) => {
|
||||
replace_call_on_arg_by_arg_method_call("items", args, expr, patch)?
|
||||
}
|
||||
("viewitems", args, []) => {
|
||||
replace_call_on_arg_by_arg_method_call("items", args, expr, patch)?
|
||||
}
|
||||
("iterkeys", args, []) => {
|
||||
replace_call_on_arg_by_arg_method_call("keys", args, expr, patch)?
|
||||
}
|
||||
("viewkeys", args, []) => {
|
||||
replace_call_on_arg_by_arg_method_call("keys", args, expr, patch)?
|
||||
}
|
||||
("itervalues", args, []) => {
|
||||
replace_call_on_arg_by_arg_method_call("values", args, expr, patch)?
|
||||
}
|
||||
("viewvalues", args, []) => {
|
||||
replace_call_on_arg_by_arg_method_call("values", args, expr, patch)?
|
||||
}
|
||||
("get_method_function", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
|
||||
"__func__", arg, expr, patch,
|
||||
)?),
|
||||
("get_method_self", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
|
||||
"__self__", arg, expr, patch,
|
||||
)?),
|
||||
("get_function_closure", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
|
||||
"__closure__",
|
||||
arg,
|
||||
expr,
|
||||
patch,
|
||||
)?),
|
||||
("get_function_code", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
|
||||
"__code__", arg, expr, patch,
|
||||
)?),
|
||||
("get_function_defaults", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
|
||||
"__defaults__",
|
||||
arg,
|
||||
expr,
|
||||
patch,
|
||||
)?),
|
||||
("get_function_globals", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
|
||||
"__globals__",
|
||||
arg,
|
||||
expr,
|
||||
patch,
|
||||
)?),
|
||||
("create_unbound_method", [arg, _], _) => {
|
||||
Some(replace_by_expr_kind(arg.node.clone(), expr, patch)?)
|
||||
}
|
||||
("get_unbound_function", [arg], []) => {
|
||||
Some(replace_by_expr_kind(arg.node.clone(), expr, patch)?)
|
||||
}
|
||||
("assertCountEqual", args, []) => {
|
||||
replace_call_on_arg_by_arg_method_call("assertCountEqual", args, expr, patch)?
|
||||
}
|
||||
("assertRaisesRegex", args, []) => {
|
||||
replace_call_on_arg_by_arg_method_call("assertRaisesRegex", args, expr, patch)?
|
||||
}
|
||||
("assertRegex", args, []) => {
|
||||
replace_call_on_arg_by_arg_method_call("assertRegex", args, expr, patch)?
|
||||
}
|
||||
("raise_from", [exc, cause], []) => Some(replace_by_raise_from(
|
||||
Some(exc.node.clone()),
|
||||
Some(cause.node.clone()),
|
||||
expr,
|
||||
patch,
|
||||
)?),
|
||||
("reraise", args, []) => handle_reraise(args, expr, patch)?,
|
||||
("byte2int", [arg], []) => Some(replace_by_index_on_arg(
|
||||
arg,
|
||||
&ExprKind::Constant {
|
||||
value: Constant::Int(0.into()),
|
||||
kind: None,
|
||||
},
|
||||
expr,
|
||||
patch,
|
||||
)?),
|
||||
("indexbytes", [arg, index], []) => {
|
||||
Some(replace_by_index_on_arg(arg, &index.node, expr, patch)?)
|
||||
}
|
||||
("int2byte", [arg], []) => Some(replace_by_expr_kind(
|
||||
ExprKind::Call {
|
||||
func: Box::new(create_expr(ExprKind::Name {
|
||||
id: "bytes".to_string(),
|
||||
ctx: ExprContext::Load,
|
||||
})),
|
||||
args: vec![create_expr(ExprKind::Tuple {
|
||||
elts: vec![create_expr(arg.node.clone())],
|
||||
ctx: ExprContext::Load,
|
||||
})],
|
||||
keywords: vec![],
|
||||
},
|
||||
expr,
|
||||
patch,
|
||||
)?),
|
||||
_ => None,
|
||||
};
|
||||
Ok(check)
|
||||
}
|
||||
|
||||
fn handle_next_on_six_dict(expr: &Expr, patch: bool, checker: &Checker) -> Result<Option<Check>> {
|
||||
let ExprKind::Call { func, args, .. } = &expr.node else {
|
||||
return Ok(None);
|
||||
};
|
||||
let ExprKind::Name { id, .. } = &func.node else {
|
||||
return Ok(None);
|
||||
};
|
||||
if id != "next" {
|
||||
return Ok(None);
|
||||
}
|
||||
let [arg] = &args[..] else { return Ok(None); };
|
||||
let call_path = dealias_call_path(collect_call_paths(arg), &checker.import_aliases);
|
||||
if !is_module_member(&call_path, "six") {
|
||||
return Ok(None);
|
||||
}
|
||||
let ExprKind::Call { func, args, .. } = &arg.node else {return Ok(None);};
|
||||
let ExprKind::Attribute { attr, .. } = &func.node else {return Ok(None);};
|
||||
let [dict_arg] = &args[..] else {return Ok(None);};
|
||||
let method_name = match attr.as_str() {
|
||||
"iteritems" => "items",
|
||||
"iterkeys" => "keys",
|
||||
"itervalues" => "values",
|
||||
_ => return Ok(None),
|
||||
};
|
||||
match replace_by_expr_kind(
|
||||
ExprKind::Call {
|
||||
func: Box::new(create_expr(ExprKind::Name {
|
||||
id: "iter".to_string(),
|
||||
ctx: ExprContext::Load,
|
||||
})),
|
||||
args: vec![create_expr(ExprKind::Call {
|
||||
func: Box::new(create_expr(ExprKind::Attribute {
|
||||
value: Box::new(dict_arg.clone()),
|
||||
attr: method_name.to_string(),
|
||||
ctx: ExprContext::Load,
|
||||
})),
|
||||
args: vec![],
|
||||
keywords: vec![],
|
||||
})],
|
||||
keywords: vec![],
|
||||
},
|
||||
arg,
|
||||
patch,
|
||||
) {
|
||||
Ok(check) => Ok(Some(check)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// UP016
|
||||
pub fn remove_six_compat(checker: &mut Checker, expr: &Expr) {
|
||||
match handle_next_on_six_dict(expr, checker.patch(&CheckCode::UP016), checker) {
|
||||
Ok(Some(check)) => {
|
||||
checker.add_check(check);
|
||||
return;
|
||||
}
|
||||
Ok(None) => (),
|
||||
Err(err) => {
|
||||
error!("Error while removing `six` reference: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let call_path = dealias_call_path(collect_call_paths(expr), &checker.import_aliases);
|
||||
if is_module_member(&call_path, "six") {
|
||||
let patch = checker.patch(&CheckCode::UP016);
|
||||
let check = match &expr.node {
|
||||
ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
} => match handle_func(func, args, keywords, expr, patch, checker.locator) {
|
||||
Ok(check) => check,
|
||||
Err(err) => {
|
||||
error!("Failed to remove `six` reference: {err}");
|
||||
return;
|
||||
}
|
||||
},
|
||||
ExprKind::Attribute { attr, .. } => map_name(attr.as_str(), expr, patch),
|
||||
ExprKind::Name { id, .. } => map_name(id.as_str(), expr, patch),
|
||||
_ => return,
|
||||
};
|
||||
if let Some(check) = check {
|
||||
checker.add_check(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,12 @@ pub fn useless_metaclass_type(checker: &mut Checker, stmt: &Stmt, value: &Expr,
|
||||
let deleted: Vec<&Stmt> = checker.deletions.iter().map(|node| node.0).collect();
|
||||
let defined_by = checker.current_parent();
|
||||
let defined_in = checker.current_grandparent();
|
||||
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());
|
||||
|
||||
@@ -0,0 +1,770 @@
|
||||
---
|
||||
source: src/pyupgrade/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 7
|
||||
column: 0
|
||||
end_location:
|
||||
row: 7
|
||||
column: 13
|
||||
fix:
|
||||
content: str
|
||||
location:
|
||||
row: 7
|
||||
column: 0
|
||||
end_location:
|
||||
row: 7
|
||||
column: 13
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 8
|
||||
column: 0
|
||||
end_location:
|
||||
row: 8
|
||||
column: 15
|
||||
fix:
|
||||
content: bytes
|
||||
location:
|
||||
row: 8
|
||||
column: 0
|
||||
end_location:
|
||||
row: 8
|
||||
column: 15
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 9
|
||||
column: 0
|
||||
end_location:
|
||||
row: 9
|
||||
column: 15
|
||||
fix:
|
||||
content: "(type,)"
|
||||
location:
|
||||
row: 9
|
||||
column: 0
|
||||
end_location:
|
||||
row: 9
|
||||
column: 15
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 10
|
||||
column: 0
|
||||
end_location:
|
||||
row: 10
|
||||
column: 16
|
||||
fix:
|
||||
content: "(str,)"
|
||||
location:
|
||||
row: 10
|
||||
column: 0
|
||||
end_location:
|
||||
row: 10
|
||||
column: 16
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 11
|
||||
column: 0
|
||||
end_location:
|
||||
row: 11
|
||||
column: 17
|
||||
fix:
|
||||
content: "(int,)"
|
||||
location:
|
||||
row: 11
|
||||
column: 0
|
||||
end_location:
|
||||
row: 11
|
||||
column: 17
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 12
|
||||
column: 0
|
||||
end_location:
|
||||
row: 12
|
||||
column: 10
|
||||
fix:
|
||||
content: chr
|
||||
location:
|
||||
row: 12
|
||||
column: 0
|
||||
end_location:
|
||||
row: 12
|
||||
column: 10
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 13
|
||||
column: 0
|
||||
end_location:
|
||||
row: 13
|
||||
column: 13
|
||||
fix:
|
||||
content: iter
|
||||
location:
|
||||
row: 13
|
||||
column: 0
|
||||
end_location:
|
||||
row: 13
|
||||
column: 13
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 14
|
||||
column: 0
|
||||
end_location:
|
||||
row: 14
|
||||
column: 10
|
||||
fix:
|
||||
content: print
|
||||
location:
|
||||
row: 14
|
||||
column: 0
|
||||
end_location:
|
||||
row: 14
|
||||
column: 10
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 15
|
||||
column: 0
|
||||
end_location:
|
||||
row: 15
|
||||
column: 9
|
||||
fix:
|
||||
content: exec
|
||||
location:
|
||||
row: 15
|
||||
column: 0
|
||||
end_location:
|
||||
row: 15
|
||||
column: 9
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 16
|
||||
column: 0
|
||||
end_location:
|
||||
row: 16
|
||||
column: 20
|
||||
fix:
|
||||
content: next
|
||||
location:
|
||||
row: 16
|
||||
column: 0
|
||||
end_location:
|
||||
row: 16
|
||||
column: 20
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 17
|
||||
column: 0
|
||||
end_location:
|
||||
row: 17
|
||||
column: 8
|
||||
fix:
|
||||
content: next
|
||||
location:
|
||||
row: 17
|
||||
column: 0
|
||||
end_location:
|
||||
row: 17
|
||||
column: 8
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 18
|
||||
column: 0
|
||||
end_location:
|
||||
row: 18
|
||||
column: 12
|
||||
fix:
|
||||
content: callable
|
||||
location:
|
||||
row: 18
|
||||
column: 0
|
||||
end_location:
|
||||
row: 18
|
||||
column: 12
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 19
|
||||
column: 0
|
||||
end_location:
|
||||
row: 19
|
||||
column: 15
|
||||
fix:
|
||||
content: range
|
||||
location:
|
||||
row: 19
|
||||
column: 0
|
||||
end_location:
|
||||
row: 19
|
||||
column: 15
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 20
|
||||
column: 0
|
||||
end_location:
|
||||
row: 20
|
||||
column: 16
|
||||
fix:
|
||||
content: range
|
||||
location:
|
||||
row: 20
|
||||
column: 0
|
||||
end_location:
|
||||
row: 20
|
||||
column: 16
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 21
|
||||
column: 16
|
||||
end_location:
|
||||
row: 21
|
||||
column: 31
|
||||
fix:
|
||||
content: "(type,)"
|
||||
location:
|
||||
row: 21
|
||||
column: 16
|
||||
end_location:
|
||||
row: 21
|
||||
column: 31
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 22
|
||||
column: 16
|
||||
end_location:
|
||||
row: 22
|
||||
column: 33
|
||||
fix:
|
||||
content: "(int,)"
|
||||
location:
|
||||
row: 22
|
||||
column: 16
|
||||
end_location:
|
||||
row: 22
|
||||
column: 33
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 23
|
||||
column: 16
|
||||
end_location:
|
||||
row: 23
|
||||
column: 32
|
||||
fix:
|
||||
content: "(str,)"
|
||||
location:
|
||||
row: 23
|
||||
column: 16
|
||||
end_location:
|
||||
row: 23
|
||||
column: 32
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 26
|
||||
column: 0
|
||||
end_location:
|
||||
row: 26
|
||||
column: 18
|
||||
fix:
|
||||
content: dct.items()
|
||||
location:
|
||||
row: 26
|
||||
column: 0
|
||||
end_location:
|
||||
row: 26
|
||||
column: 18
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 27
|
||||
column: 0
|
||||
end_location:
|
||||
row: 27
|
||||
column: 17
|
||||
fix:
|
||||
content: dct.keys()
|
||||
location:
|
||||
row: 27
|
||||
column: 0
|
||||
end_location:
|
||||
row: 27
|
||||
column: 17
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 28
|
||||
column: 0
|
||||
end_location:
|
||||
row: 28
|
||||
column: 19
|
||||
fix:
|
||||
content: dct.values()
|
||||
location:
|
||||
row: 28
|
||||
column: 0
|
||||
end_location:
|
||||
row: 28
|
||||
column: 19
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 29
|
||||
column: 0
|
||||
end_location:
|
||||
row: 29
|
||||
column: 18
|
||||
fix:
|
||||
content: dct.items()
|
||||
location:
|
||||
row: 29
|
||||
column: 0
|
||||
end_location:
|
||||
row: 29
|
||||
column: 18
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 30
|
||||
column: 0
|
||||
end_location:
|
||||
row: 30
|
||||
column: 17
|
||||
fix:
|
||||
content: dct.keys()
|
||||
location:
|
||||
row: 30
|
||||
column: 0
|
||||
end_location:
|
||||
row: 30
|
||||
column: 17
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 31
|
||||
column: 0
|
||||
end_location:
|
||||
row: 31
|
||||
column: 19
|
||||
fix:
|
||||
content: dct.values()
|
||||
location:
|
||||
row: 31
|
||||
column: 0
|
||||
end_location:
|
||||
row: 31
|
||||
column: 19
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 32
|
||||
column: 0
|
||||
end_location:
|
||||
row: 32
|
||||
column: 34
|
||||
fix:
|
||||
content: "self.assertCountEqual(a1, a2)"
|
||||
location:
|
||||
row: 32
|
||||
column: 0
|
||||
end_location:
|
||||
row: 32
|
||||
column: 34
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 33
|
||||
column: 0
|
||||
end_location:
|
||||
row: 33
|
||||
column: 37
|
||||
fix:
|
||||
content: "self.assertRaisesRegex(e, r, fn)"
|
||||
location:
|
||||
row: 33
|
||||
column: 0
|
||||
end_location:
|
||||
row: 33
|
||||
column: 37
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 34
|
||||
column: 0
|
||||
end_location:
|
||||
row: 34
|
||||
column: 27
|
||||
fix:
|
||||
content: "self.assertRegex(s, r)"
|
||||
location:
|
||||
row: 34
|
||||
column: 0
|
||||
end_location:
|
||||
row: 34
|
||||
column: 27
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 37
|
||||
column: 0
|
||||
end_location:
|
||||
row: 37
|
||||
column: 29
|
||||
fix:
|
||||
content: meth.__func__
|
||||
location:
|
||||
row: 37
|
||||
column: 0
|
||||
end_location:
|
||||
row: 37
|
||||
column: 29
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 38
|
||||
column: 0
|
||||
end_location:
|
||||
row: 38
|
||||
column: 25
|
||||
fix:
|
||||
content: meth.__self__
|
||||
location:
|
||||
row: 38
|
||||
column: 0
|
||||
end_location:
|
||||
row: 38
|
||||
column: 25
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 39
|
||||
column: 0
|
||||
end_location:
|
||||
row: 39
|
||||
column: 28
|
||||
fix:
|
||||
content: fn.__closure__
|
||||
location:
|
||||
row: 39
|
||||
column: 0
|
||||
end_location:
|
||||
row: 39
|
||||
column: 28
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 40
|
||||
column: 0
|
||||
end_location:
|
||||
row: 40
|
||||
column: 25
|
||||
fix:
|
||||
content: fn.__code__
|
||||
location:
|
||||
row: 40
|
||||
column: 0
|
||||
end_location:
|
||||
row: 40
|
||||
column: 25
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 41
|
||||
column: 0
|
||||
end_location:
|
||||
row: 41
|
||||
column: 29
|
||||
fix:
|
||||
content: fn.__defaults__
|
||||
location:
|
||||
row: 41
|
||||
column: 0
|
||||
end_location:
|
||||
row: 41
|
||||
column: 29
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 42
|
||||
column: 0
|
||||
end_location:
|
||||
row: 42
|
||||
column: 28
|
||||
fix:
|
||||
content: fn.__globals__
|
||||
location:
|
||||
row: 42
|
||||
column: 0
|
||||
end_location:
|
||||
row: 42
|
||||
column: 28
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 45
|
||||
column: 0
|
||||
end_location:
|
||||
row: 45
|
||||
column: 12
|
||||
fix:
|
||||
content: "b\"...\""
|
||||
location:
|
||||
row: 45
|
||||
column: 0
|
||||
end_location:
|
||||
row: 45
|
||||
column: 12
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 46
|
||||
column: 0
|
||||
end_location:
|
||||
row: 46
|
||||
column: 12
|
||||
fix:
|
||||
content: "\"...\""
|
||||
location:
|
||||
row: 46
|
||||
column: 0
|
||||
end_location:
|
||||
row: 46
|
||||
column: 12
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 47
|
||||
column: 0
|
||||
end_location:
|
||||
row: 47
|
||||
column: 24
|
||||
fix:
|
||||
content: "b\"...\""
|
||||
location:
|
||||
row: 47
|
||||
column: 0
|
||||
end_location:
|
||||
row: 47
|
||||
column: 24
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 48
|
||||
column: 0
|
||||
end_location:
|
||||
row: 48
|
||||
column: 21
|
||||
fix:
|
||||
content: "\"...\""
|
||||
location:
|
||||
row: 48
|
||||
column: 0
|
||||
end_location:
|
||||
row: 48
|
||||
column: 21
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 49
|
||||
column: 0
|
||||
end_location:
|
||||
row: 49
|
||||
column: 22
|
||||
fix:
|
||||
content: "\"...\""
|
||||
location:
|
||||
row: 49
|
||||
column: 0
|
||||
end_location:
|
||||
row: 49
|
||||
column: 22
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 53
|
||||
column: 0
|
||||
end_location:
|
||||
row: 53
|
||||
column: 30
|
||||
fix:
|
||||
content: meth
|
||||
location:
|
||||
row: 53
|
||||
column: 0
|
||||
end_location:
|
||||
row: 53
|
||||
column: 30
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 54
|
||||
column: 0
|
||||
end_location:
|
||||
row: 54
|
||||
column: 34
|
||||
fix:
|
||||
content: fn
|
||||
location:
|
||||
row: 54
|
||||
column: 0
|
||||
end_location:
|
||||
row: 54
|
||||
column: 34
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 57
|
||||
column: 0
|
||||
end_location:
|
||||
row: 57
|
||||
column: 29
|
||||
fix:
|
||||
content: raise exc from exc_from
|
||||
location:
|
||||
row: 57
|
||||
column: 0
|
||||
end_location:
|
||||
row: 57
|
||||
column: 29
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 58
|
||||
column: 0
|
||||
end_location:
|
||||
row: 58
|
||||
column: 24
|
||||
fix:
|
||||
content: raise exc.with_traceback(tb)
|
||||
location:
|
||||
row: 58
|
||||
column: 0
|
||||
end_location:
|
||||
row: 58
|
||||
column: 24
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 59
|
||||
column: 0
|
||||
end_location:
|
||||
row: 59
|
||||
column: 28
|
||||
fix:
|
||||
content: raise
|
||||
location:
|
||||
row: 59
|
||||
column: 0
|
||||
end_location:
|
||||
row: 59
|
||||
column: 28
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 62
|
||||
column: 0
|
||||
end_location:
|
||||
row: 62
|
||||
column: 16
|
||||
fix:
|
||||
content: "bs[0]"
|
||||
location:
|
||||
row: 62
|
||||
column: 0
|
||||
end_location:
|
||||
row: 62
|
||||
column: 16
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 63
|
||||
column: 0
|
||||
end_location:
|
||||
row: 63
|
||||
column: 21
|
||||
fix:
|
||||
content: "bs[i]"
|
||||
location:
|
||||
row: 63
|
||||
column: 0
|
||||
end_location:
|
||||
row: 63
|
||||
column: 21
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 64
|
||||
column: 0
|
||||
end_location:
|
||||
row: 64
|
||||
column: 15
|
||||
fix:
|
||||
content: "bytes((i,))"
|
||||
location:
|
||||
row: 64
|
||||
column: 0
|
||||
end_location:
|
||||
row: 64
|
||||
column: 15
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 67
|
||||
column: 5
|
||||
end_location:
|
||||
row: 67
|
||||
column: 23
|
||||
fix:
|
||||
content: iter(dct.items())
|
||||
location:
|
||||
row: 67
|
||||
column: 5
|
||||
end_location:
|
||||
row: 67
|
||||
column: 23
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 67
|
||||
column: 5
|
||||
end_location:
|
||||
row: 67
|
||||
column: 23
|
||||
fix:
|
||||
content: dct.items()
|
||||
location:
|
||||
row: 67
|
||||
column: 5
|
||||
end_location:
|
||||
row: 67
|
||||
column: 23
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 68
|
||||
column: 5
|
||||
end_location:
|
||||
row: 68
|
||||
column: 22
|
||||
fix:
|
||||
content: iter(dct.keys())
|
||||
location:
|
||||
row: 68
|
||||
column: 5
|
||||
end_location:
|
||||
row: 68
|
||||
column: 22
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 68
|
||||
column: 5
|
||||
end_location:
|
||||
row: 68
|
||||
column: 22
|
||||
fix:
|
||||
content: dct.keys()
|
||||
location:
|
||||
row: 68
|
||||
column: 5
|
||||
end_location:
|
||||
row: 68
|
||||
column: 22
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 69
|
||||
column: 5
|
||||
end_location:
|
||||
row: 69
|
||||
column: 24
|
||||
fix:
|
||||
content: iter(dct.values())
|
||||
location:
|
||||
row: 69
|
||||
column: 5
|
||||
end_location:
|
||||
row: 69
|
||||
column: 24
|
||||
- kind: RemoveSixCompat
|
||||
location:
|
||||
row: 69
|
||||
column: 5
|
||||
end_location:
|
||||
row: 69
|
||||
column: 24
|
||||
fix:
|
||||
content: dct.values()
|
||||
location:
|
||||
row: 69
|
||||
column: 5
|
||||
end_location:
|
||||
row: 69
|
||||
column: 24
|
||||
|
||||
214
src/resolver.rs
214
src/resolver.rs
@@ -3,21 +3,30 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use ignore::{DirEntry, WalkBuilder, WalkState};
|
||||
use log::debug;
|
||||
use path_absolutize::path_dedot;
|
||||
use rustc_hash::FxHashSet;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::cli::Overrides;
|
||||
use crate::fs;
|
||||
use crate::settings::configuration::Configuration;
|
||||
use crate::settings::pyproject::has_ruff_section;
|
||||
use crate::settings::{pyproject, Settings};
|
||||
|
||||
/// The strategy for discovering a `pyproject.toml` file for each Python file.
|
||||
/// The strategy used to discover Python files in the filesystem..
|
||||
#[derive(Debug)]
|
||||
pub enum Strategy {
|
||||
pub struct FileDiscovery {
|
||||
pub respect_gitignore: bool,
|
||||
}
|
||||
|
||||
/// The strategy used to discover the relevant `pyproject.toml` file for each
|
||||
/// Python file.
|
||||
#[derive(Debug)]
|
||||
pub enum PyprojectDiscovery {
|
||||
/// Use a fixed `pyproject.toml` file for all Python files (i.e., one
|
||||
/// provided on the command-line).
|
||||
Fixed(Settings),
|
||||
@@ -64,10 +73,10 @@ impl Resolver {
|
||||
}
|
||||
|
||||
/// Return the appropriate `Settings` for a given `Path`.
|
||||
pub fn resolve<'a>(&'a self, path: &Path, strategy: &'a Strategy) -> &'a Settings {
|
||||
pub fn resolve<'a>(&'a self, path: &Path, strategy: &'a PyprojectDiscovery) -> &'a Settings {
|
||||
match strategy {
|
||||
Strategy::Fixed(settings) => settings,
|
||||
Strategy::Hierarchical(default) => self
|
||||
PyprojectDiscovery::Fixed(settings) => settings,
|
||||
PyprojectDiscovery::Hierarchical(default) => self
|
||||
.settings
|
||||
.iter()
|
||||
.rev()
|
||||
@@ -164,99 +173,138 @@ fn is_excluded(file_path: &str, file_basename: &str, exclude: &globset::GlobSet)
|
||||
}
|
||||
|
||||
/// Return `true` if the `Path` appears to be that of a Python file.
|
||||
fn is_python_file(path: &Path) -> bool {
|
||||
fn is_python_path(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.map_or(false, |ext| ext == "py" || ext == "pyi")
|
||||
}
|
||||
|
||||
/// Find all Python (`.py` and `.pyi` files) in a set of `PathBuf`s.
|
||||
pub fn resolve_python_files(
|
||||
paths: &[PathBuf],
|
||||
strategy: &Strategy,
|
||||
overrides: &Overrides,
|
||||
) -> Result<(Vec<Result<DirEntry, walkdir::Error>>, Resolver)> {
|
||||
let mut files = Vec::new();
|
||||
let mut resolver = Resolver::default();
|
||||
for path in paths {
|
||||
let (files_in_path, file_resolver) = python_files_in_path(path, strategy, overrides)?;
|
||||
files.extend(files_in_path);
|
||||
resolver.merge(file_resolver);
|
||||
}
|
||||
Ok((files, resolver))
|
||||
/// Return `true` if the `Entry` appears to be that of a Python file.
|
||||
pub fn is_python_entry(entry: &DirEntry) -> bool {
|
||||
is_python_path(entry.path())
|
||||
&& !entry
|
||||
.file_type()
|
||||
.map_or(false, |file_type| file_type.is_dir())
|
||||
}
|
||||
|
||||
/// Find all Python (`.py` and `.pyi` files) in a given `Path`.
|
||||
fn python_files_in_path(
|
||||
path: &Path,
|
||||
strategy: &Strategy,
|
||||
/// Find all Python (`.py` and `.pyi` files) in a set of paths.
|
||||
pub fn python_files_in_path(
|
||||
paths: &[PathBuf],
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
file_strategy: &FileDiscovery,
|
||||
overrides: &Overrides,
|
||||
) -> Result<(Vec<Result<DirEntry, walkdir::Error>>, Resolver)> {
|
||||
let path = fs::normalize_path(path);
|
||||
) -> Result<(Vec<Result<DirEntry, ignore::Error>>, Resolver)> {
|
||||
// Normalize every path (e.g., convert from relative to absolute).
|
||||
let paths: Vec<PathBuf> = paths.iter().map(|path| fs::normalize_path(path)).collect();
|
||||
|
||||
// Search for `pyproject.toml` files in all parent directories.
|
||||
let mut resolver = Resolver::default();
|
||||
for path in path.ancestors() {
|
||||
if path.is_dir() {
|
||||
let pyproject = path.join("pyproject.toml");
|
||||
for path in &paths {
|
||||
for ancestor in path.ancestors() {
|
||||
let pyproject = ancestor.join("pyproject.toml");
|
||||
if pyproject.is_file() {
|
||||
let (root, settings) =
|
||||
resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
|
||||
resolver.add(root, settings);
|
||||
if has_ruff_section(&pyproject)? {
|
||||
let (root, settings) =
|
||||
resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
|
||||
resolver.add(root, settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all Python files.
|
||||
let files: Vec<Result<DirEntry, walkdir::Error>> = WalkDir::new(path)
|
||||
.into_iter()
|
||||
.filter_entry(|entry| {
|
||||
// Create the `WalkBuilder`.
|
||||
let mut builder = WalkBuilder::new(
|
||||
paths
|
||||
.get(0)
|
||||
.ok_or_else(|| anyhow!("Expected at least one path to search for Python files"))?,
|
||||
);
|
||||
for path in &paths[1..] {
|
||||
builder.add(path);
|
||||
}
|
||||
builder.standard_filters(file_strategy.respect_gitignore);
|
||||
builder.hidden(false);
|
||||
let walker = builder.build_parallel();
|
||||
|
||||
// Run the `WalkParallel` to collect all Python files.
|
||||
let error: std::sync::Mutex<Result<()>> = std::sync::Mutex::new(Ok(()));
|
||||
let resolver: RwLock<Resolver> = RwLock::new(resolver);
|
||||
let files: std::sync::Mutex<Vec<Result<DirEntry, ignore::Error>>> =
|
||||
std::sync::Mutex::new(vec![]);
|
||||
walker.run(|| {
|
||||
Box::new(|result| {
|
||||
// Search for the `pyproject.toml` file in this directory, before we visit any
|
||||
// of its contents.
|
||||
if entry.file_type().is_dir() {
|
||||
let pyproject = entry.path().join("pyproject.toml");
|
||||
if pyproject.is_file() {
|
||||
// TODO(charlie): Return a `Result` here.
|
||||
let (root, settings) =
|
||||
resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))
|
||||
.unwrap();
|
||||
resolver.add(root, settings);
|
||||
}
|
||||
}
|
||||
|
||||
let path = entry.path();
|
||||
let settings = resolver.resolve(path, strategy);
|
||||
match fs::extract_path_names(path) {
|
||||
Ok((file_path, file_basename)) => {
|
||||
if !settings.exclude.is_empty()
|
||||
&& is_excluded(file_path, file_basename, &settings.exclude)
|
||||
{
|
||||
debug!("Ignored path via `exclude`: {:?}", path);
|
||||
false
|
||||
} else if !settings.extend_exclude.is_empty()
|
||||
&& is_excluded(file_path, file_basename, &settings.extend_exclude)
|
||||
{
|
||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
if let Ok(entry) = &result {
|
||||
if entry
|
||||
.file_type()
|
||||
.map_or(false, |file_type| file_type.is_dir())
|
||||
{
|
||||
let pyproject = entry.path().join("pyproject.toml");
|
||||
if pyproject.is_file() {
|
||||
match has_ruff_section(&pyproject) {
|
||||
Ok(false) => {}
|
||||
Ok(true) => {
|
||||
match resolve_scoped_settings(
|
||||
&pyproject,
|
||||
&Relativity::Parent,
|
||||
Some(overrides),
|
||||
) {
|
||||
Ok((root, settings)) => {
|
||||
resolver.write().unwrap().add(root, settings);
|
||||
}
|
||||
Err(err) => {
|
||||
*error.lock().unwrap() = Err(err);
|
||||
return WalkState::Quit;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
*error.lock().unwrap() = Err(err);
|
||||
return WalkState::Quit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Ignored path due to error in parsing: {:?}: {}", path, e);
|
||||
true
|
||||
}
|
||||
|
||||
// Respect our own exclusion behavior.
|
||||
if let Ok(entry) = &result {
|
||||
if entry.depth() > 0 {
|
||||
let path = entry.path();
|
||||
let resolver = resolver.read().unwrap();
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
match fs::extract_path_names(path) {
|
||||
Ok((file_path, file_basename)) => {
|
||||
if !settings.exclude.is_empty()
|
||||
&& is_excluded(file_path, file_basename, &settings.exclude)
|
||||
{
|
||||
debug!("Ignored path via `exclude`: {:?}", path);
|
||||
return WalkState::Skip;
|
||||
} else if !settings.extend_exclude.is_empty()
|
||||
&& is_excluded(file_path, file_basename, &settings.extend_exclude)
|
||||
{
|
||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||
return WalkState::Skip;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("Ignored path due to error in parsing: {:?}: {}", path, err);
|
||||
return WalkState::Skip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(|entry| {
|
||||
entry.as_ref().map_or(true, |entry| {
|
||||
(entry.depth() == 0 || is_python_file(entry.path()))
|
||||
&& !entry.file_type().is_dir()
|
||||
&& !(entry.file_type().is_symlink() && entry.path().is_dir())
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok((files, resolver))
|
||||
if result.as_ref().map_or(true, is_python_entry) {
|
||||
files.lock().unwrap().push(result);
|
||||
}
|
||||
|
||||
WalkState::Continue
|
||||
})
|
||||
});
|
||||
|
||||
error.into_inner().unwrap()?;
|
||||
|
||||
Ok((files.into_inner().unwrap(), resolver.into_inner().unwrap()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -268,22 +316,22 @@ mod tests {
|
||||
use path_absolutize::Absolutize;
|
||||
|
||||
use crate::fs;
|
||||
use crate::resolver::{is_excluded, is_python_file};
|
||||
use crate::resolver::{is_excluded, is_python_path};
|
||||
use crate::settings::types::FilePattern;
|
||||
|
||||
#[test]
|
||||
fn inclusions() {
|
||||
let path = Path::new("foo/bar/baz.py").absolutize().unwrap();
|
||||
assert!(is_python_file(&path));
|
||||
assert!(is_python_path(&path));
|
||||
|
||||
let path = Path::new("foo/bar/baz.pyi").absolutize().unwrap();
|
||||
assert!(is_python_file(&path));
|
||||
assert!(is_python_path(&path));
|
||||
|
||||
let path = Path::new("foo/bar/baz.js").absolutize().unwrap();
|
||||
assert!(!is_python_file(&path));
|
||||
assert!(!is_python_path(&path));
|
||||
|
||||
let path = Path::new("foo/bar/baz").absolutize().unwrap();
|
||||
assert!(!is_python_file(&path));
|
||||
assert!(!is_python_path(&path));
|
||||
}
|
||||
|
||||
fn make_exclusion(file_pattern: FilePattern) -> GlobSet {
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::settings::options::Options;
|
||||
use crate::settings::pyproject::load_options;
|
||||
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat};
|
||||
use crate::{
|
||||
flake8_annotations, flake8_bugbear, flake8_import_conventions, flake8_quotes,
|
||||
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes,
|
||||
flake8_tidy_imports, fs, isort, mccabe, pep8_naming, pyupgrade,
|
||||
};
|
||||
|
||||
@@ -24,9 +24,9 @@ pub struct Configuration {
|
||||
pub dummy_variable_rgx: Option<Regex>,
|
||||
pub exclude: Option<Vec<FilePattern>>,
|
||||
pub extend: Option<PathBuf>,
|
||||
pub extend_exclude: Option<Vec<FilePattern>>,
|
||||
pub extend_ignore: Option<Vec<CheckCodePrefix>>,
|
||||
pub extend_select: Option<Vec<CheckCodePrefix>>,
|
||||
pub extend_exclude: Vec<FilePattern>,
|
||||
pub extend_ignore: Vec<Vec<CheckCodePrefix>>,
|
||||
pub extend_select: Vec<Vec<CheckCodePrefix>>,
|
||||
pub external: Option<Vec<String>>,
|
||||
pub fix: Option<bool>,
|
||||
pub fixable: Option<Vec<CheckCodePrefix>>,
|
||||
@@ -35,6 +35,7 @@ pub struct Configuration {
|
||||
pub ignore_init_module_imports: Option<bool>,
|
||||
pub line_length: Option<usize>,
|
||||
pub per_file_ignores: Option<Vec<PerFileIgnore>>,
|
||||
pub respect_gitignore: Option<bool>,
|
||||
pub select: Option<Vec<CheckCodePrefix>>,
|
||||
pub show_source: Option<bool>,
|
||||
pub src: Option<Vec<PathBuf>>,
|
||||
@@ -43,6 +44,7 @@ pub struct Configuration {
|
||||
// Plugins
|
||||
pub flake8_annotations: Option<flake8_annotations::settings::Options>,
|
||||
pub flake8_bugbear: Option<flake8_bugbear::settings::Options>,
|
||||
pub flake8_errmsg: Option<flake8_errmsg::settings::Options>,
|
||||
pub flake8_import_conventions: Option<flake8_import_conventions::settings::Options>,
|
||||
pub flake8_quotes: Option<flake8_quotes::settings::Options>,
|
||||
pub flake8_tidy_imports: Option<flake8_tidy_imports::settings::Options>,
|
||||
@@ -59,18 +61,12 @@ impl Configuration {
|
||||
|
||||
pub fn from_options(options: Options, project_root: &Path) -> Result<Self> {
|
||||
Ok(Configuration {
|
||||
extend: options.extend.map(PathBuf::from),
|
||||
allowed_confusables: options.allowed_confusables,
|
||||
dummy_variable_rgx: options
|
||||
.dummy_variable_rgx
|
||||
.map(|pattern| Regex::new(&pattern))
|
||||
.transpose()
|
||||
.map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
|
||||
src: options
|
||||
.src
|
||||
.map(|src| resolve_src(&src, project_root))
|
||||
.transpose()?,
|
||||
target_version: options.target_version,
|
||||
exclude: options.exclude.map(|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
@@ -80,22 +76,24 @@ impl Configuration {
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
extend_exclude: options.extend_exclude.map(|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|pattern| {
|
||||
let absolute = fs::normalize_path_to(Path::new(&pattern), project_root);
|
||||
FilePattern::User(pattern, absolute)
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
extend_ignore: options.extend_ignore,
|
||||
select: options.select,
|
||||
extend_select: options.extend_select,
|
||||
extend: options.extend.map(PathBuf::from),
|
||||
extend_exclude: options
|
||||
.extend_exclude
|
||||
.map(|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|pattern| {
|
||||
let absolute = fs::normalize_path_to(Path::new(&pattern), project_root);
|
||||
FilePattern::User(pattern, absolute)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
extend_ignore: vec![options.extend_ignore.unwrap_or_default()],
|
||||
extend_select: vec![options.extend_select.unwrap_or_default()],
|
||||
external: options.external,
|
||||
fix: options.fix,
|
||||
fixable: options.fixable,
|
||||
unfixable: options.unfixable,
|
||||
format: options.format,
|
||||
ignore: options.ignore,
|
||||
ignore_init_module_imports: options.ignore_init_module_imports,
|
||||
@@ -109,10 +107,19 @@ impl Configuration {
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
respect_gitignore: options.respect_gitignore,
|
||||
select: options.select,
|
||||
show_source: options.show_source,
|
||||
src: options
|
||||
.src
|
||||
.map(|src| resolve_src(&src, project_root))
|
||||
.transpose()?,
|
||||
target_version: options.target_version,
|
||||
unfixable: options.unfixable,
|
||||
// Plugins
|
||||
flake8_annotations: options.flake8_annotations,
|
||||
flake8_bugbear: options.flake8_bugbear,
|
||||
flake8_errmsg: options.flake8_errmsg,
|
||||
flake8_import_conventions: options.flake8_import_conventions,
|
||||
flake8_quotes: options.flake8_quotes,
|
||||
flake8_tidy_imports: options.flake8_tidy_imports,
|
||||
@@ -129,10 +136,23 @@ impl Configuration {
|
||||
allowed_confusables: self.allowed_confusables.or(config.allowed_confusables),
|
||||
dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx),
|
||||
exclude: self.exclude.or(config.exclude),
|
||||
respect_gitignore: self.respect_gitignore.or(config.respect_gitignore),
|
||||
extend: self.extend.or(config.extend),
|
||||
extend_exclude: self.extend_exclude.or(config.extend_exclude),
|
||||
extend_ignore: self.extend_ignore.or(config.extend_ignore),
|
||||
extend_select: self.extend_select.or(config.extend_select),
|
||||
extend_exclude: config
|
||||
.extend_exclude
|
||||
.into_iter()
|
||||
.chain(self.extend_exclude.into_iter())
|
||||
.collect(),
|
||||
extend_ignore: config
|
||||
.extend_ignore
|
||||
.into_iter()
|
||||
.chain(self.extend_ignore.into_iter())
|
||||
.collect(),
|
||||
extend_select: config
|
||||
.extend_select
|
||||
.into_iter()
|
||||
.chain(self.extend_select.into_iter())
|
||||
.collect(),
|
||||
external: self.external.or(config.external),
|
||||
fix: self.fix.or(config.fix),
|
||||
fixable: self.fixable.or(config.fixable),
|
||||
@@ -151,6 +171,7 @@ impl Configuration {
|
||||
// Plugins
|
||||
flake8_annotations: self.flake8_annotations.or(config.flake8_annotations),
|
||||
flake8_bugbear: self.flake8_bugbear.or(config.flake8_bugbear),
|
||||
flake8_errmsg: self.flake8_errmsg.or(config.flake8_errmsg),
|
||||
flake8_import_conventions: self
|
||||
.flake8_import_conventions
|
||||
.or(config.flake8_import_conventions),
|
||||
@@ -171,13 +192,7 @@ impl Configuration {
|
||||
self.exclude = Some(exclude);
|
||||
}
|
||||
if let Some(extend_exclude) = overrides.extend_exclude {
|
||||
self.extend_exclude = Some(extend_exclude);
|
||||
}
|
||||
if let Some(extend_ignore) = overrides.extend_ignore {
|
||||
self.extend_ignore = Some(extend_ignore);
|
||||
}
|
||||
if let Some(extend_select) = overrides.extend_select {
|
||||
self.extend_select = Some(extend_select);
|
||||
self.extend_exclude.extend(extend_exclude);
|
||||
}
|
||||
if let Some(fix) = overrides.fix {
|
||||
self.fix = Some(fix);
|
||||
@@ -202,6 +217,9 @@ impl Configuration {
|
||||
if let Some(per_file_ignores) = overrides.per_file_ignores {
|
||||
self.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores));
|
||||
}
|
||||
if let Some(respect_gitignore) = overrides.respect_gitignore {
|
||||
self.respect_gitignore = Some(respect_gitignore);
|
||||
}
|
||||
if let Some(select) = overrides.select {
|
||||
self.select = Some(select);
|
||||
}
|
||||
@@ -214,6 +232,23 @@ impl Configuration {
|
||||
if let Some(unfixable) = overrides.unfixable {
|
||||
self.unfixable = Some(unfixable);
|
||||
}
|
||||
// Special-case: `extend_ignore` and `extend_select` are parallel arrays, so
|
||||
// push an empty array if only one of the two is provided.
|
||||
match (overrides.extend_ignore, overrides.extend_select) {
|
||||
(Some(extend_ignore), Some(extend_select)) => {
|
||||
self.extend_ignore.push(extend_ignore);
|
||||
self.extend_select.push(extend_select);
|
||||
}
|
||||
(Some(extend_ignore), None) => {
|
||||
self.extend_ignore.push(extend_ignore);
|
||||
self.extend_select.push(Vec::new());
|
||||
}
|
||||
(None, Some(extend_select)) => {
|
||||
self.extend_ignore.push(Vec::new());
|
||||
self.extend_select.push(extend_select);
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::checks_gen::{CheckCodePrefix, SuffixLength, CATEGORIES};
|
||||
use crate::settings::configuration::Configuration;
|
||||
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat};
|
||||
use crate::{
|
||||
flake8_annotations, flake8_bugbear, flake8_import_conventions, flake8_quotes,
|
||||
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes,
|
||||
flake8_tidy_imports, isort, mccabe, pep8_naming, pyupgrade,
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ pub mod pyproject;
|
||||
pub mod types;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct Settings {
|
||||
pub allowed_confusables: FxHashSet<char>,
|
||||
pub dummy_variable_rgx: Regex,
|
||||
@@ -42,12 +43,14 @@ pub struct Settings {
|
||||
pub ignore_init_module_imports: bool,
|
||||
pub line_length: usize,
|
||||
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)>,
|
||||
pub respect_gitignore: bool,
|
||||
pub show_source: bool,
|
||||
pub src: Vec<PathBuf>,
|
||||
pub target_version: PythonVersion,
|
||||
// Plugins
|
||||
pub flake8_annotations: flake8_annotations::settings::Settings,
|
||||
pub flake8_bugbear: flake8_bugbear::settings::Settings,
|
||||
pub flake8_errmsg: flake8_errmsg::settings::Settings,
|
||||
pub flake8_import_conventions: flake8_import_conventions::settings::Settings,
|
||||
pub flake8_quotes: flake8_quotes::settings::Settings,
|
||||
pub flake8_tidy_imports: flake8_tidy_imports::settings::Settings,
|
||||
@@ -95,26 +98,31 @@ impl Settings {
|
||||
.dummy_variable_rgx
|
||||
.unwrap_or_else(|| DEFAULT_DUMMY_VARIABLE_RGX.clone()),
|
||||
enabled: resolve_codes(
|
||||
&config
|
||||
.select
|
||||
.unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F])
|
||||
.into_iter()
|
||||
.chain(config.extend_select.unwrap_or_default().into_iter())
|
||||
.collect::<Vec<_>>(),
|
||||
&config
|
||||
.ignore
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(config.extend_ignore.unwrap_or_default().into_iter())
|
||||
.collect::<Vec<_>>(),
|
||||
[CheckCodeSpec {
|
||||
select: &config
|
||||
.select
|
||||
.unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]),
|
||||
ignore: &config.ignore.unwrap_or_default(),
|
||||
}]
|
||||
.into_iter()
|
||||
.chain(
|
||||
config
|
||||
.extend_select
|
||||
.iter()
|
||||
.zip(config.extend_ignore.iter())
|
||||
.map(|(select, ignore)| CheckCodeSpec { select, ignore }),
|
||||
),
|
||||
),
|
||||
exclude: resolve_globset(config.exclude.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()))?,
|
||||
extend_exclude: resolve_globset(config.extend_exclude.unwrap_or_default())?,
|
||||
extend_exclude: resolve_globset(config.extend_exclude)?,
|
||||
external: FxHashSet::from_iter(config.external.unwrap_or_default()),
|
||||
fix: config.fix.unwrap_or(false),
|
||||
fixable: resolve_codes(
|
||||
&config.fixable.unwrap_or_else(|| CATEGORIES.to_vec()),
|
||||
&config.unfixable.unwrap_or_default(),
|
||||
[CheckCodeSpec {
|
||||
select: &config.fixable.unwrap_or_else(|| CATEGORIES.to_vec()),
|
||||
ignore: &config.unfixable.unwrap_or_default(),
|
||||
}]
|
||||
.into_iter(),
|
||||
),
|
||||
format: config.format.unwrap_or(SerializationFormat::Text),
|
||||
ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(),
|
||||
@@ -122,6 +130,7 @@ impl Settings {
|
||||
per_file_ignores: resolve_per_file_ignores(
|
||||
config.per_file_ignores.unwrap_or_default(),
|
||||
)?,
|
||||
respect_gitignore: config.respect_gitignore.unwrap_or(true),
|
||||
src: config
|
||||
.src
|
||||
.unwrap_or_else(|| vec![project_root.to_path_buf()]),
|
||||
@@ -136,6 +145,10 @@ impl Settings {
|
||||
.flake8_bugbear
|
||||
.map(flake8_bugbear::settings::Settings::from_options)
|
||||
.unwrap_or_default(),
|
||||
flake8_errmsg: config
|
||||
.flake8_errmsg
|
||||
.map(flake8_errmsg::settings::Settings::from_options)
|
||||
.unwrap_or_default(),
|
||||
flake8_import_conventions: config
|
||||
.flake8_import_conventions
|
||||
.map(flake8_import_conventions::settings::Settings::from_options)
|
||||
@@ -183,11 +196,13 @@ impl Settings {
|
||||
ignore_init_module_imports: false,
|
||||
line_length: 88,
|
||||
per_file_ignores: vec![],
|
||||
respect_gitignore: true,
|
||||
show_source: false,
|
||||
src: vec![path_dedot::CWD.clone()],
|
||||
target_version: PythonVersion::Py310,
|
||||
flake8_annotations: flake8_annotations::settings::Settings::default(),
|
||||
flake8_bugbear: flake8_bugbear::settings::Settings::default(),
|
||||
flake8_errmsg: flake8_errmsg::settings::Settings::default(),
|
||||
flake8_import_conventions: flake8_import_conventions::settings::Settings::default(),
|
||||
flake8_quotes: flake8_quotes::settings::Settings::default(),
|
||||
flake8_tidy_imports: flake8_tidy_imports::settings::Settings::default(),
|
||||
@@ -212,11 +227,13 @@ impl Settings {
|
||||
ignore_init_module_imports: false,
|
||||
line_length: 88,
|
||||
per_file_ignores: vec![],
|
||||
respect_gitignore: true,
|
||||
show_source: false,
|
||||
src: vec![path_dedot::CWD.clone()],
|
||||
target_version: PythonVersion::Py310,
|
||||
flake8_annotations: flake8_annotations::settings::Settings::default(),
|
||||
flake8_bugbear: flake8_bugbear::settings::Settings::default(),
|
||||
flake8_errmsg: flake8_errmsg::settings::Settings::default(),
|
||||
flake8_import_conventions: flake8_import_conventions::settings::Settings::default(),
|
||||
flake8_quotes: flake8_quotes::settings::Settings::default(),
|
||||
flake8_tidy_imports: flake8_tidy_imports::settings::Settings::default(),
|
||||
@@ -296,26 +313,34 @@ pub fn resolve_per_file_ignores(
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CheckCodeSpec<'a> {
|
||||
select: &'a [CheckCodePrefix],
|
||||
ignore: &'a [CheckCodePrefix],
|
||||
}
|
||||
|
||||
/// Given a set of selected and ignored prefixes, resolve the set of enabled
|
||||
/// error codes.
|
||||
fn resolve_codes(select: &[CheckCodePrefix], ignore: &[CheckCodePrefix]) -> FxHashSet<CheckCode> {
|
||||
fn resolve_codes<'a>(specs: impl Iterator<Item = CheckCodeSpec<'a>>) -> FxHashSet<CheckCode> {
|
||||
let mut codes: FxHashSet<CheckCode> = FxHashSet::default();
|
||||
for specificity in [
|
||||
SuffixLength::Zero,
|
||||
SuffixLength::One,
|
||||
SuffixLength::Two,
|
||||
SuffixLength::Three,
|
||||
SuffixLength::Four,
|
||||
] {
|
||||
for prefix in select {
|
||||
if prefix.specificity() == specificity {
|
||||
codes.extend(prefix.codes());
|
||||
for spec in specs {
|
||||
for specificity in [
|
||||
SuffixLength::Zero,
|
||||
SuffixLength::One,
|
||||
SuffixLength::Two,
|
||||
SuffixLength::Three,
|
||||
SuffixLength::Four,
|
||||
] {
|
||||
for prefix in spec.select {
|
||||
if prefix.specificity() == specificity {
|
||||
codes.extend(prefix.codes());
|
||||
}
|
||||
}
|
||||
}
|
||||
for prefix in ignore {
|
||||
if prefix.specificity() == specificity {
|
||||
for code in prefix.codes() {
|
||||
codes.remove(&code);
|
||||
for prefix in spec.ignore {
|
||||
if prefix.specificity() == specificity {
|
||||
for code in prefix.codes() {
|
||||
codes.remove(&code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,24 +354,80 @@ mod tests {
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
use crate::checks_gen::CheckCodePrefix;
|
||||
use crate::settings::resolve_codes;
|
||||
use crate::settings::{resolve_codes, CheckCodeSpec};
|
||||
|
||||
#[test]
|
||||
fn resolver() {
|
||||
let actual = resolve_codes(&[CheckCodePrefix::W], &[]);
|
||||
fn check_codes() {
|
||||
let actual = resolve_codes(
|
||||
[CheckCodeSpec {
|
||||
select: &[CheckCodePrefix::W],
|
||||
ignore: &[],
|
||||
}]
|
||||
.into_iter(),
|
||||
);
|
||||
let expected = FxHashSet::from_iter([CheckCode::W292, CheckCode::W605]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_codes(&[CheckCodePrefix::W6], &[]);
|
||||
let actual = resolve_codes(
|
||||
[CheckCodeSpec {
|
||||
select: &[CheckCodePrefix::W6],
|
||||
ignore: &[],
|
||||
}]
|
||||
.into_iter(),
|
||||
);
|
||||
let expected = FxHashSet::from_iter([CheckCode::W605]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_codes(&[CheckCodePrefix::W], &[CheckCodePrefix::W292]);
|
||||
let actual = resolve_codes(
|
||||
[CheckCodeSpec {
|
||||
select: &[CheckCodePrefix::W],
|
||||
ignore: &[CheckCodePrefix::W292],
|
||||
}]
|
||||
.into_iter(),
|
||||
);
|
||||
let expected = FxHashSet::from_iter([CheckCode::W605]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_codes(&[CheckCodePrefix::W605], &[CheckCodePrefix::W605]);
|
||||
let actual = resolve_codes(
|
||||
[CheckCodeSpec {
|
||||
select: &[CheckCodePrefix::W605],
|
||||
ignore: &[CheckCodePrefix::W605],
|
||||
}]
|
||||
.into_iter(),
|
||||
);
|
||||
let expected = FxHashSet::from_iter([]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_codes(
|
||||
[
|
||||
CheckCodeSpec {
|
||||
select: &[CheckCodePrefix::W],
|
||||
ignore: &[CheckCodePrefix::W292],
|
||||
},
|
||||
CheckCodeSpec {
|
||||
select: &[CheckCodePrefix::W292],
|
||||
ignore: &[],
|
||||
},
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
let expected = FxHashSet::from_iter([CheckCode::W292, CheckCode::W605]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_codes(
|
||||
[
|
||||
CheckCodeSpec {
|
||||
select: &[CheckCodePrefix::W],
|
||||
ignore: &[CheckCodePrefix::W292],
|
||||
},
|
||||
CheckCodeSpec {
|
||||
select: &[CheckCodePrefix::W292],
|
||||
ignore: &[CheckCodePrefix::W],
|
||||
},
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
let expected = FxHashSet::from_iter([CheckCode::W292]);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::checks_gen::CheckCodePrefix;
|
||||
use crate::settings::types::{PythonVersion, SerializationFormat};
|
||||
use crate::{
|
||||
flake8_annotations, flake8_bugbear, flake8_import_conventions, flake8_quotes,
|
||||
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes,
|
||||
flake8_tidy_imports, isort, mccabe, pep8_naming, pyupgrade,
|
||||
};
|
||||
|
||||
@@ -205,6 +205,18 @@ pub struct Options {
|
||||
"#
|
||||
)]
|
||||
pub line_length: Option<usize>,
|
||||
#[option(
|
||||
doc = r#"
|
||||
Whether to automatically exclude files that are ignored by `.ignore`, `.gitignore`,
|
||||
`.git/info/exclude`, and global `gitignore` files. Enabled by default.
|
||||
"#,
|
||||
default = "true",
|
||||
value_type = "bool",
|
||||
example = r#"
|
||||
respect_gitignore = false
|
||||
"#
|
||||
)]
|
||||
pub respect_gitignore: Option<bool>,
|
||||
#[option(
|
||||
doc = r#"
|
||||
A list of check code prefixes to enable. Prefixes can specify exact checks (like
|
||||
@@ -295,6 +307,8 @@ pub struct Options {
|
||||
#[option_group]
|
||||
pub flake8_bugbear: Option<flake8_bugbear::settings::Options>,
|
||||
#[option_group]
|
||||
pub flake8_errmsg: Option<flake8_errmsg::settings::Options>,
|
||||
#[option_group]
|
||||
pub flake8_quotes: Option<flake8_quotes::settings::Options>,
|
||||
#[option_group]
|
||||
pub flake8_tidy_imports: Option<flake8_tidy_imports::settings::Options>,
|
||||
|
||||
@@ -33,17 +33,24 @@ fn parse_pyproject_toml(path: &Path) -> Result<Pyproject> {
|
||||
toml::from_str(&contents).map_err(std::convert::Into::into)
|
||||
}
|
||||
|
||||
/// Find the nearest `pyproject.toml` file.
|
||||
pub fn find_pyproject_toml(path: &Path) -> Option<PathBuf> {
|
||||
for directory in path.ancestors() {
|
||||
let pyproject = directory.join("pyproject.toml");
|
||||
if pyproject.is_file() {
|
||||
return Some(pyproject);
|
||||
}
|
||||
}
|
||||
None
|
||||
/// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section.
|
||||
pub fn has_ruff_section(path: &Path) -> Result<bool> {
|
||||
let pyproject = parse_pyproject_toml(path)?;
|
||||
Ok(pyproject.tool.and_then(|tool| tool.ruff).is_some())
|
||||
}
|
||||
|
||||
/// Find the path to the `pyproject.toml` file, if such a file exists.
|
||||
pub fn find_pyproject_toml(path: &Path) -> Result<Option<PathBuf>> {
|
||||
for directory in path.ancestors() {
|
||||
let pyproject = directory.join("pyproject.toml");
|
||||
if pyproject.is_file() && has_ruff_section(&pyproject)? {
|
||||
return Ok(Some(pyproject));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Find the path to the user-specific `pyproject.toml`, if it exists.
|
||||
pub fn find_user_pyproject_toml() -> Option<PathBuf> {
|
||||
let mut path = dirs::config_dir()?;
|
||||
path.push("ruff");
|
||||
@@ -55,6 +62,7 @@ pub fn find_user_pyproject_toml() -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load `Options` from a `pyproject.toml`.
|
||||
pub fn load_options(pyproject: &Path) -> Result<Options> {
|
||||
Ok(parse_pyproject_toml(pyproject)
|
||||
.map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))?
|
||||
@@ -79,8 +87,8 @@ mod tests {
|
||||
};
|
||||
use crate::settings::types::PatternPrefixPair;
|
||||
use crate::{
|
||||
flake8_bugbear, flake8_import_conventions, flake8_quotes, flake8_tidy_imports, mccabe,
|
||||
pep8_naming,
|
||||
flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes,
|
||||
flake8_tidy_imports, mccabe, pep8_naming,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -119,6 +127,7 @@ mod tests {
|
||||
ignore_init_module_imports: None,
|
||||
line_length: None,
|
||||
per_file_ignores: None,
|
||||
respect_gitignore: None,
|
||||
select: None,
|
||||
show_source: None,
|
||||
src: None,
|
||||
@@ -127,6 +136,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,
|
||||
@@ -163,6 +173,7 @@ line-length = 79
|
||||
ignore_init_module_imports: None,
|
||||
line_length: Some(79),
|
||||
per_file_ignores: None,
|
||||
respect_gitignore: None,
|
||||
select: None,
|
||||
show_source: None,
|
||||
src: None,
|
||||
@@ -171,6 +182,7 @@ line-length = 79
|
||||
unfixable: None,
|
||||
flake8_annotations: None,
|
||||
flake8_bugbear: None,
|
||||
flake8_errmsg: None,
|
||||
flake8_quotes: None,
|
||||
flake8_tidy_imports: None,
|
||||
flake8_import_conventions: None,
|
||||
@@ -209,11 +221,13 @@ exclude = ["foo.py"]
|
||||
format: None,
|
||||
unfixable: None,
|
||||
per_file_ignores: None,
|
||||
respect_gitignore: None,
|
||||
dummy_variable_rgx: None,
|
||||
src: None,
|
||||
target_version: None,
|
||||
show_source: None,
|
||||
flake8_annotations: None,
|
||||
flake8_errmsg: None,
|
||||
flake8_bugbear: None,
|
||||
flake8_quotes: None,
|
||||
flake8_tidy_imports: None,
|
||||
@@ -251,6 +265,7 @@ select = ["E501"]
|
||||
ignore_init_module_imports: None,
|
||||
line_length: None,
|
||||
per_file_ignores: None,
|
||||
respect_gitignore: None,
|
||||
select: Some(vec![CheckCodePrefix::E501]),
|
||||
show_source: None,
|
||||
src: None,
|
||||
@@ -259,6 +274,7 @@ select = ["E501"]
|
||||
unfixable: None,
|
||||
flake8_annotations: None,
|
||||
flake8_bugbear: None,
|
||||
flake8_errmsg: None,
|
||||
flake8_quotes: None,
|
||||
flake8_tidy_imports: None,
|
||||
flake8_import_conventions: None,
|
||||
@@ -296,6 +312,7 @@ ignore = ["E501"]
|
||||
ignore_init_module_imports: None,
|
||||
line_length: None,
|
||||
per_file_ignores: None,
|
||||
respect_gitignore: None,
|
||||
select: None,
|
||||
show_source: None,
|
||||
src: None,
|
||||
@@ -304,6 +321,7 @@ ignore = ["E501"]
|
||||
unfixable: None,
|
||||
flake8_annotations: None,
|
||||
flake8_bugbear: None,
|
||||
flake8_errmsg: None,
|
||||
flake8_quotes: None,
|
||||
flake8_tidy_imports: None,
|
||||
flake8_import_conventions: None,
|
||||
@@ -350,7 +368,7 @@ other-attribute = 1
|
||||
fn find_and_parse_pyproject_toml() -> Result<()> {
|
||||
let cwd = current_dir()?;
|
||||
let pyproject =
|
||||
find_pyproject_toml(&cwd.join("resources/test/fixtures/__init__.py")).unwrap();
|
||||
find_pyproject_toml(&cwd.join("resources/test/fixtures/__init__.py"))?.unwrap();
|
||||
assert_eq!(
|
||||
pyproject,
|
||||
cwd.join("resources/test/fixtures/pyproject.toml")
|
||||
@@ -383,8 +401,9 @@ other-attribute = 1
|
||||
per_file_ignores: Some(FxHashMap::from_iter([(
|
||||
"__init__.py".to_string(),
|
||||
vec![CheckCodePrefix::F401]
|
||||
),])),
|
||||
)])),
|
||||
dummy_variable_rgx: None,
|
||||
respect_gitignore: None,
|
||||
src: None,
|
||||
target_version: None,
|
||||
show_source: None,
|
||||
@@ -395,6 +414,9 @@ other-attribute = 1
|
||||
"fastapi.Query".to_string(),
|
||||
]),
|
||||
}),
|
||||
flake8_errmsg: Some(flake8_errmsg::settings::Options {
|
||||
max_string_length: Some(20),
|
||||
}),
|
||||
flake8_quotes: Some(flake8_quotes::settings::Options {
|
||||
inline_quotes: Some(Quote::Single),
|
||||
multiline_quotes: Some(Quote::Double),
|
||||
|
||||
198
tests/black_compatibility_test.rs
Normal file
198
tests/black_compatibility_test.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::io::{ErrorKind, Read};
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::{fs, process, str};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assert_cmd::{crate_name, Command};
|
||||
use itertools::Itertools;
|
||||
use log::info;
|
||||
use ruff::checks::CheckCategory;
|
||||
use ruff::logging::{set_up_logging, LogLevel};
|
||||
use strum::IntoEnumIterator;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Handles `blackd` process and allows submitting code to it for formatting.
|
||||
struct Blackd {
|
||||
address: SocketAddr,
|
||||
server: process::Child,
|
||||
client: ureq::Agent,
|
||||
}
|
||||
|
||||
impl Blackd {
|
||||
pub fn new() -> Result<Self> {
|
||||
// Get free TCP port to run on
|
||||
let address = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))?.local_addr()?;
|
||||
|
||||
let server = process::Command::new("blackd")
|
||||
.args([
|
||||
"--bind-host",
|
||||
&address.ip().to_string(),
|
||||
"--bind-port",
|
||||
&address.port().to_string(),
|
||||
])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.context("Starting blackd")?;
|
||||
|
||||
// Wait for `blackd` to be ready.
|
||||
for _ in 0..10 {
|
||||
match TcpStream::connect(address) {
|
||||
Err(e) if e.kind() == ErrorKind::ConnectionRefused => {
|
||||
info!("`blackd` not ready yet; retrying...");
|
||||
sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
Ok(_) => {
|
||||
info!("`blackd` ready");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
address,
|
||||
server,
|
||||
client: ureq::agent(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Format given code with blackd.
|
||||
pub fn check(&self, code: &[u8]) -> Result<Vec<u8>> {
|
||||
match self
|
||||
.client
|
||||
.post(&format!("http://{}/", self.address))
|
||||
.set("X-Line-Length", "88")
|
||||
.send_bytes(code)
|
||||
{
|
||||
// 204 indicates the input wasn't changed during formatting, so
|
||||
// we return the original.
|
||||
Ok(response) => {
|
||||
if response.status() == 204 {
|
||||
Ok(code.to_vec())
|
||||
} else {
|
||||
let mut buf = vec![];
|
||||
response
|
||||
.into_reader()
|
||||
.take((1024 * 1024) as u64)
|
||||
.read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
Err(ureq::Error::Status(_, response)) => Err(anyhow::anyhow!(
|
||||
"Formatting with `black` failed: {}",
|
||||
response.into_string()?
|
||||
)),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Blackd {
|
||||
fn drop(&mut self) {
|
||||
self.server.kill().expect("Couldn't end `blackd` process");
|
||||
}
|
||||
}
|
||||
|
||||
fn run_test(path: &Path, blackd: &Blackd, ruff_args: &[&str]) -> Result<()> {
|
||||
let input = fs::read(path)?;
|
||||
|
||||
// Step 1: Run `ruff` on the input.
|
||||
let step_1 = &Command::cargo_bin(crate_name!())?
|
||||
.args(ruff_args)
|
||||
.write_stdin(input)
|
||||
.assert()
|
||||
.append_context("step", "running input through ruff");
|
||||
if !step_1.get_output().status.success() {
|
||||
return Err(anyhow!(
|
||||
"Running input through ruff failed:\n{}",
|
||||
str::from_utf8(&step_1.get_output().stderr)?
|
||||
));
|
||||
}
|
||||
let step_1_output = step_1.get_output().stdout.clone();
|
||||
|
||||
// Step 2: Run `blackd` on the input.
|
||||
let step_2_output = blackd.check(&step_1_output)?;
|
||||
|
||||
// Step 3: Re-run `ruff` on the input.
|
||||
let step_3 = &Command::cargo_bin(crate_name!())?
|
||||
.args(ruff_args)
|
||||
.write_stdin(step_2_output.clone())
|
||||
.assert();
|
||||
if !step_3.get_output().status.success() {
|
||||
return Err(anyhow!(
|
||||
"Running input through ruff after black failed:\n{}",
|
||||
str::from_utf8(&step_3.get_output().stderr)?
|
||||
));
|
||||
}
|
||||
let step_3_output = step_3.get_output().stdout.clone();
|
||||
|
||||
assert_eq!(
|
||||
str::from_utf8(&step_2_output),
|
||||
str::from_utf8(&step_3_output),
|
||||
"Mismatch found for {}",
|
||||
path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_ruff_black_compatibility() -> Result<()> {
|
||||
set_up_logging(&LogLevel::Default)?;
|
||||
|
||||
let blackd = Blackd::new()?;
|
||||
|
||||
let fixtures_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources")
|
||||
.join("test")
|
||||
.join("fixtures");
|
||||
|
||||
// Ignore some fixtures that currently trigger errors. `E999.py` especially, as
|
||||
// that is triggering a syntax error on purpose.
|
||||
let excludes = ["E999.py"];
|
||||
|
||||
let paths: Vec<walkdir::DirEntry> = WalkDir::new(fixtures_dir)
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
entry.as_ref().map_or(true, |entry| {
|
||||
entry
|
||||
.path()
|
||||
.extension()
|
||||
.map_or(false, |ext| ext == "py" || ext == "pyi")
|
||||
&& !excludes.contains(&entry.path().file_name().unwrap().to_str().unwrap())
|
||||
})
|
||||
})
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
|
||||
let codes = CheckCategory::iter()
|
||||
// Exclude ruff codes, specifically RUF100, because it causes differences that are not a
|
||||
// problem. Ruff would add a `# noqa: W292` after the first run, black introduces a
|
||||
// newline, and ruff removes the `# noqa: W292` again.
|
||||
.filter(|category| *category != CheckCategory::Ruff)
|
||||
.map(|category| category.codes().iter().map(AsRef::as_ref).join(","))
|
||||
.join(",");
|
||||
let ruff_args = [
|
||||
"-",
|
||||
"--silent",
|
||||
"--exit-zero",
|
||||
"--fix",
|
||||
"--line-length",
|
||||
"88",
|
||||
"--select",
|
||||
&codes,
|
||||
];
|
||||
|
||||
for entry in paths {
|
||||
let path = entry.path();
|
||||
run_test(path, &blackd, &ruff_args).context(format!("Testing {}", path.display()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user