Compare commits

...

22 Commits

Author SHA1 Message Date
Charlie Marsh
a9f56ee76e Bump version to 0.0.183 2022-12-15 23:15:12 -05:00
Charlie Marsh
b4bfa87104 Avoid removing partially-unused imports (#1259) 2022-12-15 23:13:58 -05:00
Charlie Marsh
b9f42bf5e5 Remove extraneous test file 2022-12-15 23:12:19 -05:00
Edgar R. M
8281d414ca Implement flake8-errmsg (#1258) 2022-12-15 23:10:59 -05:00
Charlie Marsh
7e45a9f2e2 Avoid generating invalid statements when deleting from multi-statement lines (#1253) 2022-12-15 22:17:31 -05:00
Reiner Gerecke
a000cd4a09 Test to prevent continious reformatting when used together with black (#1206) 2022-12-15 15:26:41 -05:00
Martin Lehoux
d8b4b92733 Implement U016: Remove six compatibility code (#1013) 2022-12-15 14:16:58 -05:00
Edgar R. M
27de342e75 Implement pandas-vet (#1235) 2022-12-15 14:01:01 -05:00
Charlie Marsh
d805067683 Avoid fixing E711 and E712 issues that would cause F632 (#1248) 2022-12-15 12:08:31 -05:00
Charlie Marsh
1ea2e93f8e Bump version to 0.0.182 2022-12-14 22:57:22 -05:00
Charlie Marsh
dc180dc277 Negate ignore_names condition 2022-12-14 22:50:26 -05:00
Charlie Marsh
6be910ae07 Use more precise ranges for function and class checks (#1247) 2022-12-14 22:40:00 -05:00
Charlie Marsh
ba85eb846c Run cargo fmt 2022-12-14 21:52:44 -05:00
Charlie Marsh
d067efe265 Treat extend-* configuration options as "always extended" (#1245) 2022-12-14 20:22:40 -05:00
Charlie Marsh
549ea2f85f Ignore any pyproject.toml without a [tool.ruff] section (#1243) 2022-12-14 19:35:52 -05:00
Charlie Marsh
d814ebd21f Bump version to 0.0.181 2022-12-14 17:35:36 -05:00
Charlie Marsh
3f272b6cf8 Enable opt-out of .gitignore checks via respect-gitignore flag (#1242) 2022-12-14 16:54:23 -05:00
Charlie Marsh
76891a8c07 Always check zero-depth CLI paths (#1241) 2022-12-14 16:32:02 -05:00
Charlie Marsh
e389201b5f Add new .gitignore behavior to BREAKING_CHANGES.md (#1240) 2022-12-14 16:04:06 -05:00
Charlie Marsh
4b2020d03a Automatically ignore files specified in .gitignore (#1234) 2022-12-14 15:58:40 -05:00
Charlie Marsh
0aa356c96c Avoid converting expression to statement in invald contexts (#1239) 2022-12-14 13:57:25 -05:00
Charlie Marsh
630b4b627d Apply fix to all errors in E711 and E712 autofix (#1238) 2022-12-14 13:29:56 -05:00
85 changed files with 4459 additions and 665 deletions

View File

@@ -115,7 +115,9 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- run: pip install black[d]==22.12.0
- run: cargo test --all
- run: cargo test --package ruff --test black_compatibility_test -- --ignored
maturin_build:
name: "maturin build"

View File

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

View File

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

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

View File

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

@@ -86,6 +86,7 @@ of [Conda](https://docs.conda.io/en/latest/):
1. [flake8-builtins (A)](#flake8-builtins-a)
1. [flake8-comprehensions (C4)](#flake8-comprehensions-c4)
1. [flake8-debugger (T10)](#flake8-debugger-t10)
1. [flake8-errmsg (EM)](#flake8-errmsg-em)
1. [flake8-import-conventions (ICN)](#flake8-import-conventions-icn)
1. [flake8-print (T20)](#flake8-print-t20)
1. [flake8-quotes (Q)](#flake8-quotes-q)
@@ -94,6 +95,7 @@ of [Conda](https://docs.conda.io/en/latest/):
1. [flake8-tidy-imports (TID)](#flake8-tidy-imports-tid)
1. [flake8-unused-arguments (ARG)](#flake8-unused-arguments-arg)
1. [eradicate (ERA)](#eradicate-era)
1. [pandas-vet (PDV)](#pandas-vet-pdv)
1. [pygrep-hooks (PGH)](#pygrep-hooks-pgh)
1. [Pylint (PLC, PLE, PLR, PLW)](#pylint-plc-ple-plr-plw)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf)<!-- End auto-generated table of contents. -->
@@ -155,7 +157,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.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)

View File

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

View File

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

View File

@@ -7,7 +7,8 @@ use ruff::flake8_tidy_imports::settings::Strictness;
use ruff::settings::options::Options;
use ruff::settings::pyproject::Pyproject;
use ruff::{
flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming,
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_quotes, flake8_tidy_imports, mccabe,
pep8_naming,
};
use crate::plugin::Plugin;
@@ -73,6 +74,7 @@ pub fn convert(
let mut options = Options::default();
let mut flake8_annotations = flake8_annotations::settings::Options::default();
let mut flake8_bugbear = flake8_bugbear::settings::Options::default();
let mut flake8_errmsg = flake8_errmsg::settings::Options::default();
let mut flake8_quotes = flake8_quotes::settings::Options::default();
let mut flake8_tidy_imports = flake8_tidy_imports::settings::Options::default();
let mut mccabe = mccabe::settings::Options::default();
@@ -194,6 +196,15 @@ pub fn convert(
Ok(max_complexity) => mccabe.max_complexity = Some(max_complexity),
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
},
// flake8-errmsg
"errmsg-max-string-length" | "errmsg_max_string_length" => {
match value.clone().parse::<usize>() {
Ok(max_string_length) => {
flake8_errmsg.max_string_length = Some(max_string_length);
}
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
}
}
// Unknown
_ => eprintln!("Skipping unsupported property: {key}"),
}
@@ -209,6 +220,9 @@ pub fn convert(
if flake8_bugbear != flake8_bugbear::settings::Options::default() {
options.flake8_bugbear = Some(flake8_bugbear);
}
if flake8_errmsg != flake8_errmsg::settings::Options::default() {
options.flake8_errmsg = Some(flake8_errmsg);
}
if flake8_quotes != flake8_quotes::settings::Options::default() {
options.flake8_quotes = Some(flake8_quotes);
}
@@ -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,

View File

@@ -14,6 +14,7 @@ pub enum Plugin {
Flake8Comprehensions,
Flake8Debugger,
Flake8Docstrings,
Flake8ErrMsg,
Flake8Eradicate,
Flake8Print,
Flake8Quotes,
@@ -21,6 +22,7 @@ pub enum Plugin {
Flake8Simplify,
Flake8TidyImports,
McCabe,
PandasVet,
PEP8Naming,
Pyupgrade,
}
@@ -39,12 +41,14 @@ impl FromStr for Plugin {
"flake8-debugger" => Ok(Plugin::Flake8Debugger),
"flake8-docstrings" => Ok(Plugin::Flake8Docstrings),
"flake8-eradicate" => Ok(Plugin::Flake8BlindExcept),
"flake8-errmsg" => Ok(Plugin::Flake8ErrMsg),
"flake8-print" => Ok(Plugin::Flake8Print),
"flake8-quotes" => Ok(Plugin::Flake8Quotes),
"flake8-return" => Ok(Plugin::Flake8Return),
"flake8-simplify" => Ok(Plugin::Flake8Simplify),
"flake8-tidy-imports" => Ok(Plugin::Flake8TidyImports),
"mccabe" => Ok(Plugin::McCabe),
"pandas-vet" => Ok(Plugin::PandasVet),
"pep8-naming" => Ok(Plugin::PEP8Naming),
"pyupgrade" => Ok(Plugin::Pyupgrade),
_ => Err(anyhow!("Unknown plugin: {string}")),
@@ -57,19 +61,24 @@ impl Plugin {
match self {
Plugin::Flake8Annotations => CheckCodePrefix::ANN,
Plugin::Flake8Bandit => CheckCodePrefix::S,
// TODO(charlie): Handle rename of `B` to `BLE`.
Plugin::Flake8BlindExcept => CheckCodePrefix::BLE,
Plugin::Flake8Bugbear => CheckCodePrefix::B,
Plugin::Flake8Builtins => CheckCodePrefix::A,
Plugin::Flake8Comprehensions => CheckCodePrefix::C4,
Plugin::Flake8Debugger => CheckCodePrefix::T1,
Plugin::Flake8Docstrings => CheckCodePrefix::D,
// TODO(charlie): Handle rename of `E` to `ERA`.
Plugin::Flake8Eradicate => CheckCodePrefix::ERA,
Plugin::Flake8ErrMsg => CheckCodePrefix::EM,
Plugin::Flake8Print => CheckCodePrefix::T2,
Plugin::Flake8Quotes => CheckCodePrefix::Q,
Plugin::Flake8Return => CheckCodePrefix::RET,
Plugin::Flake8Simplify => CheckCodePrefix::SIM,
Plugin::Flake8TidyImports => CheckCodePrefix::I25,
Plugin::McCabe => CheckCodePrefix::C9,
// TODO(charlie): Handle rename of `PD` to `PDV`.
Plugin::PandasVet => CheckCodePrefix::PDV,
Plugin::PEP8Naming => CheckCodePrefix::N,
Plugin::Pyupgrade => CheckCodePrefix::U,
}
@@ -101,14 +110,16 @@ impl Plugin {
DocstringConvention::PEP8.select()
}
Plugin::Flake8Eradicate => vec![CheckCodePrefix::ERA],
Plugin::Flake8ErrMsg => vec![CheckCodePrefix::EM],
Plugin::Flake8Print => vec![CheckCodePrefix::T2],
Plugin::Flake8Quotes => vec![CheckCodePrefix::Q],
Plugin::Flake8Return => vec![CheckCodePrefix::RET],
Plugin::Flake8Simplify => vec![CheckCodePrefix::SIM],
Plugin::Flake8TidyImports => vec![CheckCodePrefix::I25],
Plugin::Flake8TidyImports => vec![CheckCodePrefix::TID],
Plugin::McCabe => vec![CheckCodePrefix::C9],
Plugin::PandasVet => vec![CheckCodePrefix::PDV],
Plugin::PEP8Naming => vec![CheckCodePrefix::N],
Plugin::Pyupgrade => vec![CheckCodePrefix::U],
Plugin::Pyupgrade => vec![CheckCodePrefix::UP],
}
}
}
@@ -377,6 +388,9 @@ pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> V
"staticmethod-decorators" | "staticmethod_decorators" => {
plugins.insert(Plugin::PEP8Naming);
}
"max-string-length" | "max_string_length" => {
plugins.insert(Plugin::Flake8ErrMsg);
}
_ => {}
}
}
@@ -398,11 +412,13 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin
Plugin::Flake8Debugger,
Plugin::Flake8Docstrings,
Plugin::Flake8Eradicate,
Plugin::Flake8ErrMsg,
Plugin::Flake8Print,
Plugin::Flake8Quotes,
Plugin::Flake8Return,
Plugin::Flake8Simplify,
Plugin::Flake8TidyImports,
Plugin::PandasVet,
Plugin::PEP8Naming,
Plugin::Pyupgrade,
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View 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"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

@@ -83,7 +83,15 @@ pub(crate) fn check_path(
match rustpython_helpers::parse_program_tokens(tokens, "<filename>") {
Ok(python_ast) => {
if use_ast {
checks.extend(check_ast(&python_ast, locator, settings, autofix, path));
checks.extend(check_ast(
&python_ast,
locator,
&directives.noqa_line_for,
settings,
autofix,
ignore_noqa,
path,
));
}
if use_imports {
checks.extend(check_imports(

View File

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

View File

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

View File

@@ -8,10 +8,10 @@ expression: checks
- 1
location:
row: 2
column: 0
column: 4
end_location:
row: 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: ~

View File

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

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

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

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

View File

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

View File

@@ -6,45 +6,45 @@ expression: checks
InvalidClassName: bad
location:
row: 1
column: 0
column: 6
end_location:
row: 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: ~

View File

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

View File

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

View File

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

View File

@@ -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(())
}
}

View File

@@ -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(),
));

View File

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

View File

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

View File

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

View File

@@ -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)?;

View File

@@ -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<()> {

View File

@@ -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()) {

View File

@@ -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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
}
}
}

View File

@@ -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());

View File

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

View File

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

View File

@@ -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) => {}
}
}
}

View File

@@ -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);
}
}

View File

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

View File

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

View 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(())
}