Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ea2e93f8e | ||
|
|
dc180dc277 | ||
|
|
6be910ae07 | ||
|
|
ba85eb846c | ||
|
|
d067efe265 | ||
|
|
549ea2f85f | ||
|
|
d814ebd21f | ||
|
|
3f272b6cf8 | ||
|
|
76891a8c07 | ||
|
|
e389201b5f | ||
|
|
4b2020d03a | ||
|
|
0aa356c96c | ||
|
|
630b4b627d |
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.180
|
||||
rev: v0.0.182
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.0.181
|
||||
|
||||
### Files excluded by `.gitignore` are now ignored ([#1234](https://github.com/charliermarsh/ruff/pull/1234))
|
||||
|
||||
Ruff will now avoid checking files that are excluded by `.ignore`, `.gitignore`,
|
||||
`.git/info/exclude`, and global `gitignore` files. This behavior is powered by the [`ignore`](https://docs.rs/ignore/latest/ignore/struct.WalkBuilder.html#ignore-rules)
|
||||
crate, and is applied in addition to Ruff's built-in `exclude` system.
|
||||
|
||||
To disable this behavior, set `respect-gitignore = false` in your `pyproject.toml` file.
|
||||
|
||||
Note that hidden files (i.e., files and directories prefixed with a `.`) are _not_ ignored by
|
||||
default.
|
||||
|
||||
## 0.0.178
|
||||
|
||||
### Configuration files are now resolved hierarchically ([#1190](https://github.com/charliermarsh/ruff/pull/1190))
|
||||
|
||||
36
Cargo.lock
generated
36
Cargo.lock
generated
@@ -724,7 +724,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.180-dev.0"
|
||||
version = "0.0.182-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.182"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -1849,6 +1867,7 @@ dependencies = [
|
||||
"getrandom 0.2.8",
|
||||
"glob",
|
||||
"globset",
|
||||
"ignore",
|
||||
"insta",
|
||||
"itertools",
|
||||
"libcst",
|
||||
@@ -1881,7 +1900,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.180"
|
||||
version = "0.0.182"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.29",
|
||||
@@ -1899,7 +1918,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_macros"
|
||||
version = "0.0.180"
|
||||
version = "0.0.182"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2304,6 +2323,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.45"
|
||||
|
||||
@@ -6,7 +6,7 @@ members = [
|
||||
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.180"
|
||||
version = "0.0.182"
|
||||
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.182", 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" }
|
||||
|
||||
55
README.md
55
README.md
@@ -155,7 +155,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.182
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -304,13 +304,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 +343,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 +370,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
|
||||
@@ -1707,6 +1724,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
|
||||
|
||||
4
flake8_to_ruff/Cargo.lock
generated
4
flake8_to_ruff/Cargo.lock
generated
@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8_to_ruff"
|
||||
version = "0.0.180"
|
||||
version = "0.0.182"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1975,7 +1975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.180"
|
||||
version = "0.0.182"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.180-dev.0"
|
||||
version = "0.0.182-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -258,6 +258,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,
|
||||
@@ -304,6 +305,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,
|
||||
@@ -350,6 +352,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,
|
||||
@@ -396,6 +399,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,
|
||||
@@ -442,6 +446,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,
|
||||
@@ -496,6 +501,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,
|
||||
@@ -578,6 +584,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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
1
resources/test/project/.gitignore
vendored
Normal file
1
resources/test/project/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
examples/generated
|
||||
@@ -9,33 +9,37 @@ Running from the repo root should pick up and enforce the appropriate settings f
|
||||
|
||||
```
|
||||
∴ cargo run resources/test/project/
|
||||
Found 5 error(s).
|
||||
Found 7 error(s).
|
||||
resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
|
||||
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
|
||||
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
resources/test/project/src/file.py:1:8: F401 `os` imported but unused
|
||||
resources/test/project/src/file.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
3 potentially fixable with the --fix option.
|
||||
6 potentially fixable with the --fix option.
|
||||
```
|
||||
|
||||
Running from the project directory itself should exhibit the same behavior:
|
||||
|
||||
```
|
||||
∴ cd resources/test/project/ && cargo run .
|
||||
Found 5 error(s).
|
||||
∴ (cd resources/test/project/ && cargo run .)
|
||||
Found 7 error(s).
|
||||
examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
|
||||
examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
|
||||
examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
src/file.py:1:8: F401 `os` imported but unused
|
||||
src/file.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
3 potentially fixable with the --fix option.
|
||||
6 potentially fixable with the --fix option.
|
||||
```
|
||||
|
||||
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
|
||||
files:
|
||||
|
||||
```
|
||||
∴ cd resources/test/project/examples/docs && cargo run .
|
||||
∴ (cd resources/test/project/examples/docs && cargo run .)
|
||||
Found 2 error(s).
|
||||
docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
@@ -46,28 +50,40 @@ docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
file paths from the current working directory:
|
||||
|
||||
```
|
||||
∴ cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/
|
||||
Found 9 error(s).
|
||||
∴ (cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/)
|
||||
Found 11 error(s).
|
||||
resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
|
||||
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
|
||||
resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused
|
||||
resources/test/project/examples/docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but unused
|
||||
resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but unused
|
||||
resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused
|
||||
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
|
||||
resources/test/project/src/file.py:1:8: F401 `os` imported but unused
|
||||
resources/test/project/src/file.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
6 potentially fixable with the --fix option.
|
||||
11 potentially fixable with the --fix option.
|
||||
```
|
||||
|
||||
Running from a parent directory should this "ignore" the `exclude` (hence, `concepts/file.py` gets
|
||||
included in the output):
|
||||
|
||||
```
|
||||
∴ cd resources/test/project/examples && cargo run -- --config=docs/pyproject.toml .
|
||||
Found 3 error(s).
|
||||
∴ (cd resources/test/project/examples && cargo run -- --config=docs/pyproject.toml .)
|
||||
Found 4 error(s).
|
||||
docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
excluded/script.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
1 potentially fixable with the --fix option.
|
||||
```
|
||||
|
||||
Passing an excluded directory directly should report errors in the contained files:
|
||||
|
||||
```
|
||||
∴ cargo run resources/test/project/examples/excluded/
|
||||
Found 1 error(s).
|
||||
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
|
||||
1 potentially fixable with the --fix option.
|
||||
```
|
||||
|
||||
2
resources/test/project/examples/.dotfiles/script.py
Executable file
2
resources/test/project/examples/.dotfiles/script.py
Executable file
@@ -0,0 +1,2 @@
|
||||
import numpy as np
|
||||
from app import app_file
|
||||
@@ -1,6 +1,7 @@
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
src = ["."]
|
||||
extend-select = ["I001"]
|
||||
# Enable I001, and re-enable F841, to test extension priority.
|
||||
extend-select = ["I001", "F841"]
|
||||
extend-ignore = ["F401"]
|
||||
extend-exclude = ["./docs/concepts/file.py"]
|
||||
|
||||
5
resources/test/project/examples/excluded/script.py
Executable file
5
resources/test/project/examples/excluded/script.py
Executable file
@@ -0,0 +1,5 @@
|
||||
import os
|
||||
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
@@ -1,3 +1,5 @@
|
||||
[tool.ruff]
|
||||
src = [".", "python_modules/*"]
|
||||
exclude = ["examples/excluded"]
|
||||
extend-select = ["I001"]
|
||||
extend-ignore = ["F841"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.180"
|
||||
version = "0.0.182"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_macros"
|
||||
version = "0.0.180"
|
||||
version = "0.0.182"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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,
|
||||
};
|
||||
use rustpython_parser::lexer;
|
||||
use rustpython_parser::lexer::Tok;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::SourceCodeLocator;
|
||||
@@ -311,13 +314,42 @@ 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)
|
||||
}
|
||||
|
||||
#[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};
|
||||
use crate::ast::types::Range;
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
|
||||
#[test]
|
||||
fn builtin() -> Result<()> {
|
||||
@@ -461,4 +493,91 @@ mod tests {
|
||||
));
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,4 @@ impl Fix {
|
||||
end_location: at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dummy(location: Location) -> Self {
|
||||
Self {
|
||||
content: String::new(),
|
||||
location,
|
||||
end_location: location,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
115
src/check_ast.rs
115
src/check_ast.rs
@@ -6,7 +6,7 @@ use itertools::Itertools;
|
||||
use log::error;
|
||||
use nohash_hasher::IntMap;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustpython_ast::Location;
|
||||
use rustpython_ast::{Located, Location};
|
||||
use rustpython_parser::ast::{
|
||||
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
|
||||
KeywordData, Operator, Stmt, StmtKind, Suite,
|
||||
@@ -257,12 +257,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 +308,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 +367,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 +405,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 +447,7 @@ where
|
||||
name,
|
||||
body,
|
||||
self.settings.mccabe.max_complexity,
|
||||
self.locator,
|
||||
) {
|
||||
self.add_check(check);
|
||||
}
|
||||
@@ -460,7 +463,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 +551,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 +578,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 +620,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 +688,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 +701,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 +714,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 +726,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 +737,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 +878,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 +947,7 @@ where
|
||||
stmt,
|
||||
&alias.node.name,
|
||||
asname,
|
||||
self.locator,
|
||||
)
|
||||
{
|
||||
self.add_check(check);
|
||||
@@ -939,6 +960,7 @@ where
|
||||
stmt,
|
||||
&alias.node.name,
|
||||
asname,
|
||||
self.locator,
|
||||
)
|
||||
{
|
||||
self.add_check(check);
|
||||
@@ -951,6 +973,7 @@ where
|
||||
stmt,
|
||||
&alias.node.name,
|
||||
asname,
|
||||
self.locator,
|
||||
)
|
||||
{
|
||||
self.add_check(check);
|
||||
@@ -962,6 +985,7 @@ where
|
||||
stmt,
|
||||
&alias.node.name,
|
||||
asname,
|
||||
self.locator,
|
||||
) {
|
||||
self.add_check(check);
|
||||
}
|
||||
@@ -972,6 +996,7 @@ where
|
||||
stmt,
|
||||
&alias.node.name,
|
||||
asname,
|
||||
self.locator,
|
||||
) {
|
||||
self.add_check(check);
|
||||
}
|
||||
@@ -1422,15 +1447,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);
|
||||
}
|
||||
@@ -1557,14 +1581,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);
|
||||
@@ -2420,19 +2437,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 +2557,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3591,12 +3598,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 +3613,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 +3622,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);
|
||||
|
||||
13
src/cli.rs
13
src/cli.rs
@@ -86,10 +86,16 @@ pub struct Cli {
|
||||
show_source: bool,
|
||||
#[clap(long, overrides_with("show_source"), hide = true)]
|
||||
no_show_source: bool,
|
||||
/// Respect file exclusions via `.gitignore` and other standard ignore
|
||||
/// files.
|
||||
#[arg(long, overrides_with("no_respect_gitignore"))]
|
||||
respect_gitignore: bool,
|
||||
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
|
||||
no_respect_gitignore: bool,
|
||||
/// See the files Ruff will be run against with the current settings.
|
||||
#[arg(long)]
|
||||
pub show_files: bool,
|
||||
/// See the settings Ruff used for the first matching file.
|
||||
/// See the settings Ruff will use to check a given Python file.
|
||||
#[arg(long)]
|
||||
pub show_settings: bool,
|
||||
/// Enable automatic additions of noqa directives to failing lines.
|
||||
@@ -156,6 +162,10 @@ impl Cli {
|
||||
line_length: self.line_length,
|
||||
max_complexity: self.max_complexity,
|
||||
per_file_ignores: self.per_file_ignores,
|
||||
respect_gitignore: resolve_bool_arg(
|
||||
self.respect_gitignore,
|
||||
self.no_respect_gitignore,
|
||||
),
|
||||
select: self.select,
|
||||
show_source: resolve_bool_arg(self.show_source, self.no_show_source),
|
||||
target_version: self.target_version,
|
||||
@@ -212,6 +222,7 @@ pub struct Overrides {
|
||||
pub line_length: Option<usize>,
|
||||
pub max_complexity: Option<usize>,
|
||||
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
|
||||
pub respect_gitignore: Option<bool>,
|
||||
pub select: Option<Vec<CheckCodePrefix>>,
|
||||
pub show_source: Option<bool>,
|
||||
pub target_version: Option<PythonVersion>,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use ignore::Error;
|
||||
use itertools::Itertools;
|
||||
use log::{debug, error};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
@@ -17,20 +18,22 @@ use crate::iterators::par_iter;
|
||||
use crate::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics};
|
||||
use crate::message::Message;
|
||||
use crate::resolver;
|
||||
use crate::resolver::Strategy;
|
||||
use crate::resolver::{FileDiscovery, PyprojectDiscovery};
|
||||
use crate::settings::types::SerializationFormat;
|
||||
|
||||
/// Run the linter over a collection of files.
|
||||
pub fn run(
|
||||
files: &[PathBuf],
|
||||
strategy: &Strategy,
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
file_strategy: &FileDiscovery,
|
||||
overrides: &Overrides,
|
||||
cache: bool,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Result<Diagnostics> {
|
||||
// Collect all the files to check.
|
||||
let start = Instant::now();
|
||||
let (paths, resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
|
||||
let (paths, resolver) =
|
||||
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
||||
let duration = start.elapsed();
|
||||
debug!("Identified files to lint in: {:?}", duration);
|
||||
|
||||
@@ -40,19 +43,23 @@ pub fn run(
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
let path = entry.path();
|
||||
let settings = resolver.resolve(path, strategy);
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
lint_path(path, settings, &cache.into(), autofix)
|
||||
.map_err(|e| (Some(path.to_owned()), e.to_string()))
|
||||
}
|
||||
Err(e) => Err((
|
||||
e.path().map(Path::to_owned),
|
||||
if let Error::WithPath { path, .. } = e {
|
||||
Some(path.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
e.io_error()
|
||||
.map_or_else(|| e.to_string(), io::Error::to_string),
|
||||
)),
|
||||
}
|
||||
.unwrap_or_else(|(path, message)| {
|
||||
if let Some(path) = &path {
|
||||
let settings = resolver.resolve(path, strategy);
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
if settings.enabled.contains(&CheckCode::E902) {
|
||||
Diagnostics::new(vec![Message {
|
||||
kind: CheckKind::IOError(message),
|
||||
@@ -93,14 +100,14 @@ fn read_from_stdin() -> Result<String> {
|
||||
|
||||
/// Run the linter over a single file, read from `stdin`.
|
||||
pub fn run_stdin(
|
||||
strategy: &Strategy,
|
||||
strategy: &PyprojectDiscovery,
|
||||
filename: &Path,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Result<Diagnostics> {
|
||||
let stdin = read_from_stdin()?;
|
||||
let settings = match strategy {
|
||||
Strategy::Fixed(settings) => settings,
|
||||
Strategy::Hierarchical(settings) => settings,
|
||||
PyprojectDiscovery::Fixed(settings) => settings,
|
||||
PyprojectDiscovery::Hierarchical(settings) => settings,
|
||||
};
|
||||
let mut diagnostics = lint_stdin(filename, &stdin, settings, autofix)?;
|
||||
diagnostics.messages.sort_unstable();
|
||||
@@ -108,10 +115,16 @@ pub fn run_stdin(
|
||||
}
|
||||
|
||||
/// Add `noqa` directives to a collection of files.
|
||||
pub fn add_noqa(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<usize> {
|
||||
pub fn add_noqa(
|
||||
files: &[PathBuf],
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
file_strategy: &FileDiscovery,
|
||||
overrides: &Overrides,
|
||||
) -> Result<usize> {
|
||||
// Collect all the files to check.
|
||||
let start = Instant::now();
|
||||
let (paths, resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
|
||||
let (paths, resolver) =
|
||||
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
||||
let duration = start.elapsed();
|
||||
debug!("Identified files to lint in: {:?}", duration);
|
||||
|
||||
@@ -120,7 +133,7 @@ pub fn add_noqa(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
let settings = resolver.resolve(path, strategy);
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
match add_noqa_to_path(path, settings) {
|
||||
Ok(count) => Some(count),
|
||||
Err(e) => {
|
||||
@@ -138,10 +151,16 @@ pub fn add_noqa(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -
|
||||
}
|
||||
|
||||
/// Automatically format a collection of files.
|
||||
pub fn autoformat(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<usize> {
|
||||
pub fn autoformat(
|
||||
files: &[PathBuf],
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
file_strategy: &FileDiscovery,
|
||||
overrides: &Overrides,
|
||||
) -> Result<usize> {
|
||||
// Collect all the files to format.
|
||||
let start = Instant::now();
|
||||
let (paths, resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
|
||||
let (paths, resolver) =
|
||||
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
||||
let duration = start.elapsed();
|
||||
debug!("Identified files to lint in: {:?}", duration);
|
||||
|
||||
@@ -150,7 +169,7 @@ pub fn autoformat(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides)
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
let settings = resolver.resolve(path, strategy);
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
match autoformat_path(path, settings) {
|
||||
Ok(()) => Some(()),
|
||||
Err(e) => {
|
||||
@@ -168,9 +187,15 @@ pub fn autoformat(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides)
|
||||
}
|
||||
|
||||
/// Print the user-facing configuration settings.
|
||||
pub fn show_settings(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<()> {
|
||||
pub fn show_settings(
|
||||
files: &[PathBuf],
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
file_strategy: &FileDiscovery,
|
||||
overrides: &Overrides,
|
||||
) -> Result<()> {
|
||||
// Collect all files in the hierarchy.
|
||||
let (paths, resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
|
||||
let (paths, resolver) =
|
||||
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
||||
|
||||
// Print the list of files.
|
||||
let Some(entry) = paths
|
||||
@@ -180,7 +205,7 @@ pub fn show_settings(files: &[PathBuf], strategy: &Strategy, overrides: &Overrid
|
||||
bail!("No files found under the given path");
|
||||
};
|
||||
let path = entry.path();
|
||||
let settings = resolver.resolve(path, strategy);
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
println!("Resolved settings for: {path:?}");
|
||||
println!("{settings:#?}");
|
||||
|
||||
@@ -188,9 +213,15 @@ pub fn show_settings(files: &[PathBuf], strategy: &Strategy, overrides: &Overrid
|
||||
}
|
||||
|
||||
/// Show the list of files to be checked based on current settings.
|
||||
pub fn show_files(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<()> {
|
||||
pub fn show_files(
|
||||
files: &[PathBuf],
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
file_strategy: &FileDiscovery,
|
||||
overrides: &Overrides,
|
||||
) -> Result<()> {
|
||||
// Collect all files in the hierarchy.
|
||||
let (paths, _resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
|
||||
let (paths, _resolver) =
|
||||
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
||||
|
||||
// Print the list of files.
|
||||
for entry in paths
|
||||
|
||||
@@ -56,16 +56,23 @@ pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
|
||||
if KWLIST.contains(&name.as_str()) {
|
||||
return;
|
||||
}
|
||||
let mut check = Check::new(CheckKind::SetAttrWithConstant, Range::from_located(expr));
|
||||
if checker.patch(check.kind.code()) {
|
||||
match assignment(obj, name, value) {
|
||||
Ok(content) => check.amend(Fix::replacement(
|
||||
content,
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
)),
|
||||
Err(e) => error!("Failed to fix invalid comparison: {e}"),
|
||||
};
|
||||
// We can only replace a `setattr` call (which is an `Expr`) with an assignment
|
||||
// (which is a `Stmt`) if the `Expr` is already being used as a `Stmt`
|
||||
// (i.e., it's directly within an `StmtKind::Expr`).
|
||||
if let StmtKind::Expr { value: child } = &checker.current_parent().0.node {
|
||||
if expr == child.as_ref() {
|
||||
let mut check = Check::new(CheckKind::SetAttrWithConstant, Range::from_located(expr));
|
||||
if checker.patch(check.kind.code()) {
|
||||
match assignment(obj, name, value) {
|
||||
Ok(content) => check.amend(Fix::replacement(
|
||||
content,
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
)),
|
||||
Err(e) => error!("Failed to fix invalid comparison: {e}"),
|
||||
};
|
||||
}
|
||||
checker.add_check(check);
|
||||
}
|
||||
}
|
||||
checker.add_check(check);
|
||||
}
|
||||
|
||||
@@ -77,4 +77,19 @@ expression: checks
|
||||
end_location:
|
||||
row: 22
|
||||
column: 31
|
||||
- kind: GetAttrWithConstant
|
||||
location:
|
||||
row: 23
|
||||
column: 3
|
||||
end_location:
|
||||
row: 23
|
||||
column: 20
|
||||
fix:
|
||||
content: x.bar
|
||||
location:
|
||||
row: 23
|
||||
column: 3
|
||||
end_location:
|
||||
row: 23
|
||||
column: 20
|
||||
|
||||
|
||||
@@ -4,77 +4,77 @@ expression: checks
|
||||
---
|
||||
- kind: SetAttrWithConstant
|
||||
location:
|
||||
row: 33
|
||||
row: 37
|
||||
column: 0
|
||||
end_location:
|
||||
row: 33
|
||||
row: 37
|
||||
column: 25
|
||||
fix:
|
||||
content: foo.bar = None
|
||||
location:
|
||||
row: 33
|
||||
row: 37
|
||||
column: 0
|
||||
end_location:
|
||||
row: 33
|
||||
row: 37
|
||||
column: 25
|
||||
- kind: SetAttrWithConstant
|
||||
location:
|
||||
row: 34
|
||||
row: 38
|
||||
column: 0
|
||||
end_location:
|
||||
row: 34
|
||||
row: 38
|
||||
column: 29
|
||||
fix:
|
||||
content: foo._123abc = None
|
||||
location:
|
||||
row: 34
|
||||
row: 38
|
||||
column: 0
|
||||
end_location:
|
||||
row: 34
|
||||
row: 38
|
||||
column: 29
|
||||
- kind: SetAttrWithConstant
|
||||
location:
|
||||
row: 35
|
||||
row: 39
|
||||
column: 0
|
||||
end_location:
|
||||
row: 35
|
||||
row: 39
|
||||
column: 28
|
||||
fix:
|
||||
content: foo.abc123 = None
|
||||
location:
|
||||
row: 35
|
||||
row: 39
|
||||
column: 0
|
||||
end_location:
|
||||
row: 35
|
||||
row: 39
|
||||
column: 28
|
||||
- kind: SetAttrWithConstant
|
||||
location:
|
||||
row: 36
|
||||
row: 40
|
||||
column: 0
|
||||
end_location:
|
||||
row: 36
|
||||
row: 40
|
||||
column: 29
|
||||
fix:
|
||||
content: foo.abc123 = None
|
||||
location:
|
||||
row: 36
|
||||
row: 40
|
||||
column: 0
|
||||
end_location:
|
||||
row: 36
|
||||
row: 40
|
||||
column: 29
|
||||
- kind: SetAttrWithConstant
|
||||
location:
|
||||
row: 37
|
||||
row: 41
|
||||
column: 0
|
||||
end_location:
|
||||
row: 37
|
||||
row: 41
|
||||
column: 30
|
||||
fix:
|
||||
content: foo.bar.baz = None
|
||||
location:
|
||||
row: 37
|
||||
row: 41
|
||||
column: 0
|
||||
end_location:
|
||||
row: 37
|
||||
row: 41
|
||||
column: 30
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
use rustpython_ast::Located;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::flake8_builtins::types::ShadowingType;
|
||||
use crate::python::builtins::BUILTINS;
|
||||
|
||||
/// Check builtin name shadowing.
|
||||
pub fn builtin_shadowing(name: &str, location: Range, node_type: ShadowingType) -> Option<Check> {
|
||||
pub fn builtin_shadowing<T>(
|
||||
name: &str,
|
||||
located: &Located<T>,
|
||||
node_type: ShadowingType,
|
||||
) -> Option<Check> {
|
||||
if BUILTINS.contains(&name) {
|
||||
Some(Check::new(
|
||||
match node_type {
|
||||
@@ -12,7 +18,7 @@ pub fn builtin_shadowing(name: &str, location: Range, node_type: ShadowingType)
|
||||
ShadowingType::Argument => CheckKind::BuiltinArgumentShadowing(name.to_string()),
|
||||
ShadowingType::Attribute => CheckKind::BuiltinAttributeShadowing(name.to_string()),
|
||||
},
|
||||
location,
|
||||
Range::from_located(located),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -87,7 +87,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() {
|
||||
|
||||
57
src/main.rs
57
src/main.rs
@@ -20,7 +20,7 @@ use ::ruff::autofix::fixer;
|
||||
use ::ruff::cli::{extract_log_level, Cli};
|
||||
use ::ruff::logging::{set_up_logging, LogLevel};
|
||||
use ::ruff::printer::Printer;
|
||||
use ::ruff::resolver::Strategy;
|
||||
use ::ruff::resolver::PyprojectDiscovery;
|
||||
use ::ruff::settings::configuration::Configuration;
|
||||
use ::ruff::settings::types::SerializationFormat;
|
||||
use ::ruff::settings::{pyproject, Settings};
|
||||
@@ -33,31 +33,31 @@ use colored::Colorize;
|
||||
use notify::{recommended_watcher, RecursiveMode, Watcher};
|
||||
use path_absolutize::path_dedot;
|
||||
use ruff::cli::Overrides;
|
||||
use ruff::resolver::{resolve_settings, Relativity};
|
||||
use ruff::resolver::{resolve_settings, FileDiscovery, Relativity};
|
||||
|
||||
/// Resolve the relevant settings strategy and defaults for the current
|
||||
/// invocation.
|
||||
fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<Strategy> {
|
||||
fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<PyprojectDiscovery> {
|
||||
if let Some(pyproject) = config {
|
||||
// First priority: the user specified a `pyproject.toml` file. Use that
|
||||
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
|
||||
// current working directory. (This matches ESLint's behavior.)
|
||||
let settings = resolve_settings(&pyproject, &Relativity::Cwd, Some(overrides))?;
|
||||
Ok(Strategy::Fixed(settings))
|
||||
} else if let Some(pyproject) = pyproject::find_pyproject_toml(path_dedot::CWD.as_path()) {
|
||||
Ok(PyprojectDiscovery::Fixed(settings))
|
||||
} else if let Some(pyproject) = pyproject::find_pyproject_toml(path_dedot::CWD.as_path())? {
|
||||
// Second priority: find a `pyproject.toml` file in the current working path,
|
||||
// and resolve all paths relative to that directory. (With
|
||||
// `Strategy::Hierarchical`, we'll end up finding the "closest" `pyproject.toml`
|
||||
// file for every Python file later on, so these act as the "default" settings.)
|
||||
let settings = resolve_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
|
||||
Ok(Strategy::Hierarchical(settings))
|
||||
Ok(PyprojectDiscovery::Hierarchical(settings))
|
||||
} else if let Some(pyproject) = pyproject::find_user_pyproject_toml() {
|
||||
// Third priority: find a user-specific `pyproject.toml`, but resolve all paths
|
||||
// relative the current working directory. (With `Strategy::Hierarchical`, we'll
|
||||
// end up the "closest" `pyproject.toml` file for every Python file later on, so
|
||||
// these act as the "default" settings.)
|
||||
let settings = resolve_settings(&pyproject, &Relativity::Cwd, Some(overrides))?;
|
||||
Ok(Strategy::Hierarchical(settings))
|
||||
Ok(PyprojectDiscovery::Hierarchical(settings))
|
||||
} else {
|
||||
// Fallback: load Ruff's default settings, and resolve all paths relative to the
|
||||
// current working directory. (With `Strategy::Hierarchical`, we'll end up the
|
||||
@@ -67,7 +67,7 @@ fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<Strategy> {
|
||||
// Apply command-line options that override defaults.
|
||||
config.apply(overrides.clone());
|
||||
let settings = Settings::from_configuration(config, &path_dedot::CWD)?;
|
||||
Ok(Strategy::Hierarchical(settings))
|
||||
Ok(PyprojectDiscovery::Hierarchical(settings))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,13 +87,19 @@ fn inner_main() -> Result<ExitCode> {
|
||||
|
||||
// Construct the "default" settings. These are used when no `pyproject.toml`
|
||||
// files are present, or files are injected from outside of the hierarchy.
|
||||
let strategy = resolve(cli.config, &overrides)?;
|
||||
let pyproject_strategy = resolve(cli.config, &overrides)?;
|
||||
|
||||
// Extract options that are included in `Settings`, but only apply at the top
|
||||
// level.
|
||||
let (fix, format) = match &strategy {
|
||||
Strategy::Fixed(settings) => (settings.fix, settings.format),
|
||||
Strategy::Hierarchical(settings) => (settings.fix, settings.format),
|
||||
let file_strategy = FileDiscovery {
|
||||
respect_gitignore: match &pyproject_strategy {
|
||||
PyprojectDiscovery::Fixed(settings) => settings.respect_gitignore,
|
||||
PyprojectDiscovery::Hierarchical(settings) => settings.respect_gitignore,
|
||||
},
|
||||
};
|
||||
let (fix, format) = match &pyproject_strategy {
|
||||
PyprojectDiscovery::Fixed(settings) => (settings.fix, settings.format),
|
||||
PyprojectDiscovery::Hierarchical(settings) => (settings.fix, settings.format),
|
||||
};
|
||||
let autofix = if fix {
|
||||
fixer::Mode::Apply
|
||||
@@ -108,11 +114,11 @@ fn inner_main() -> Result<ExitCode> {
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
if cli.show_settings {
|
||||
commands::show_settings(&cli.files, &strategy, &overrides)?;
|
||||
commands::show_settings(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
if cli.show_files {
|
||||
commands::show_files(&cli.files, &strategy, &overrides)?;
|
||||
commands::show_files(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
@@ -144,7 +150,8 @@ fn inner_main() -> Result<ExitCode> {
|
||||
|
||||
let messages = commands::run(
|
||||
&cli.files,
|
||||
&strategy,
|
||||
&pyproject_strategy,
|
||||
&file_strategy,
|
||||
&overrides,
|
||||
cache_enabled,
|
||||
&fixer::Mode::None,
|
||||
@@ -173,7 +180,8 @@ fn inner_main() -> Result<ExitCode> {
|
||||
|
||||
let messages = commands::run(
|
||||
&cli.files,
|
||||
&strategy,
|
||||
&pyproject_strategy,
|
||||
&file_strategy,
|
||||
&overrides,
|
||||
cache_enabled,
|
||||
&fixer::Mode::None,
|
||||
@@ -185,12 +193,14 @@ fn inner_main() -> Result<ExitCode> {
|
||||
}
|
||||
}
|
||||
} else if cli.add_noqa {
|
||||
let modifications = commands::add_noqa(&cli.files, &strategy, &overrides)?;
|
||||
let modifications =
|
||||
commands::add_noqa(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
|
||||
if modifications > 0 && log_level >= LogLevel::Default {
|
||||
println!("Added {modifications} noqa directives.");
|
||||
}
|
||||
} else if cli.autoformat {
|
||||
let modifications = commands::autoformat(&cli.files, &strategy, &overrides)?;
|
||||
let modifications =
|
||||
commands::autoformat(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
|
||||
if modifications > 0 && log_level >= LogLevel::Default {
|
||||
println!("Formatted {modifications} files.");
|
||||
}
|
||||
@@ -201,9 +211,16 @@ fn inner_main() -> Result<ExitCode> {
|
||||
let diagnostics = if is_stdin {
|
||||
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
|
||||
let path = Path::new(&filename);
|
||||
commands::run_stdin(&strategy, path, &autofix)?
|
||||
commands::run_stdin(&pyproject_strategy, path, &autofix)?
|
||||
} else {
|
||||
commands::run(&cli.files, &strategy, &overrides, cache_enabled, &autofix)?
|
||||
commands::run(
|
||||
&cli.files,
|
||||
&pyproject_strategy,
|
||||
&file_strategy,
|
||||
&overrides,
|
||||
cache_enabled,
|
||||
&autofix,
|
||||
)?
|
||||
};
|
||||
|
||||
// Always try to print violations (the printer itself may suppress output),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use rustpython_ast::{ExcepthandlerKind, ExprKind, Stmt, StmtKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::helpers::identifier_range;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
|
||||
fn get_complexity_number(stmts: &[Stmt]) -> usize {
|
||||
let mut complexity = 0;
|
||||
@@ -59,12 +60,13 @@ pub fn function_is_too_complex(
|
||||
name: &str,
|
||||
body: &[Stmt],
|
||||
max_complexity: usize,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Check> {
|
||||
let complexity = get_complexity_number(body) + 1;
|
||||
if complexity > max_complexity {
|
||||
Some(Check::new(
|
||||
CheckKind::FunctionIsTooComplex(name.to_string(), complexity),
|
||||
Range::from_located(stmt),
|
||||
identifier_range(stmt, locator),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -8,10 +8,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 2
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 3
|
||||
column: 8
|
||||
row: 2
|
||||
column: 11
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -19,10 +19,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 7
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 8
|
||||
column: 10
|
||||
row: 7
|
||||
column: 21
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -30,10 +30,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 12
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 15
|
||||
column: 12
|
||||
row: 12
|
||||
column: 14
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -41,10 +41,10 @@ expression: checks
|
||||
- 3
|
||||
location:
|
||||
row: 19
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 25
|
||||
column: 47
|
||||
row: 19
|
||||
column: 26
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -52,10 +52,10 @@ expression: checks
|
||||
- 3
|
||||
location:
|
||||
row: 29
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 36
|
||||
column: 47
|
||||
row: 29
|
||||
column: 14
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -63,10 +63,10 @@ expression: checks
|
||||
- 2
|
||||
location:
|
||||
row: 40
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 42
|
||||
column: 16
|
||||
row: 40
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -74,10 +74,10 @@ expression: checks
|
||||
- 2
|
||||
location:
|
||||
row: 46
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 50
|
||||
column: 19
|
||||
row: 46
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -85,10 +85,10 @@ expression: checks
|
||||
- 2
|
||||
location:
|
||||
row: 54
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 58
|
||||
column: 16
|
||||
row: 54
|
||||
column: 13
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -96,10 +96,10 @@ expression: checks
|
||||
- 3
|
||||
location:
|
||||
row: 62
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 69
|
||||
column: 7
|
||||
row: 62
|
||||
column: 20
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -107,10 +107,10 @@ expression: checks
|
||||
- 2
|
||||
location:
|
||||
row: 63
|
||||
column: 4
|
||||
column: 8
|
||||
end_location:
|
||||
row: 67
|
||||
column: 11
|
||||
row: 63
|
||||
column: 9
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -118,10 +118,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 64
|
||||
column: 8
|
||||
column: 12
|
||||
end_location:
|
||||
row: 65
|
||||
column: 16
|
||||
row: 64
|
||||
column: 13
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -129,10 +129,10 @@ expression: checks
|
||||
- 4
|
||||
location:
|
||||
row: 73
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 81
|
||||
column: 16
|
||||
row: 73
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -140,10 +140,10 @@ expression: checks
|
||||
- 3
|
||||
location:
|
||||
row: 85
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 92
|
||||
column: 16
|
||||
row: 85
|
||||
column: 22
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -151,10 +151,10 @@ expression: checks
|
||||
- 3
|
||||
location:
|
||||
row: 96
|
||||
column: 0
|
||||
column: 10
|
||||
end_location:
|
||||
row: 103
|
||||
column: 12
|
||||
row: 96
|
||||
column: 16
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -162,10 +162,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 107
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 108
|
||||
column: 17
|
||||
row: 107
|
||||
column: 20
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -173,10 +173,10 @@ expression: checks
|
||||
- 9
|
||||
location:
|
||||
row: 113
|
||||
column: 4
|
||||
column: 8
|
||||
end_location:
|
||||
row: 138
|
||||
column: 40
|
||||
row: 113
|
||||
column: 14
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -184,10 +184,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 118
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 119
|
||||
column: 20
|
||||
row: 118
|
||||
column: 17
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -195,10 +195,10 @@ expression: checks
|
||||
- 2
|
||||
location:
|
||||
row: 121
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 123
|
||||
column: 24
|
||||
row: 121
|
||||
column: 17
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -206,10 +206,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 126
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 127
|
||||
column: 20
|
||||
row: 126
|
||||
column: 17
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -217,10 +217,10 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 129
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 130
|
||||
column: 20
|
||||
row: 129
|
||||
column: 21
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -228,9 +228,9 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 132
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 133
|
||||
row: 132
|
||||
column: 20
|
||||
fix: ~
|
||||
- kind:
|
||||
@@ -239,9 +239,9 @@ expression: checks
|
||||
- 1
|
||||
location:
|
||||
row: 135
|
||||
column: 12
|
||||
column: 16
|
||||
end_location:
|
||||
row: 136
|
||||
column: 20
|
||||
row: 135
|
||||
column: 25
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ expression: checks
|
||||
- 4
|
||||
location:
|
||||
row: 73
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 81
|
||||
column: 16
|
||||
row: 73
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
FunctionIsTooComplex:
|
||||
@@ -19,9 +19,9 @@ expression: checks
|
||||
- 9
|
||||
location:
|
||||
row: 113
|
||||
column: 4
|
||||
column: 8
|
||||
end_location:
|
||||
row: 138
|
||||
column: 40
|
||||
row: 113
|
||||
column: 14
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -1,47 +1,53 @@
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
|
||||
use rustpython_ast::{Arg, Arguments, Expr, ExprKind, Stmt};
|
||||
|
||||
use crate::ast::function_type;
|
||||
use crate::ast::helpers::identifier_range;
|
||||
use crate::ast::types::{Range, Scope, ScopeKind};
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::pep8_naming::helpers;
|
||||
use crate::pep8_naming::settings::Settings;
|
||||
use crate::python::string::{self};
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
|
||||
/// N801
|
||||
pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option<Check> {
|
||||
pub fn invalid_class_name(
|
||||
class_def: &Stmt,
|
||||
name: &str,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Check> {
|
||||
let stripped = name.strip_prefix('_').unwrap_or(name);
|
||||
if !stripped.chars().next().map_or(false, char::is_uppercase) || stripped.contains('_') {
|
||||
return Some(Check::new(
|
||||
CheckKind::InvalidClassName(name.to_string()),
|
||||
Range::from_located(class_def),
|
||||
identifier_range(class_def, locator),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// N802
|
||||
pub fn invalid_function_name(func_def: &Stmt, name: &str, settings: &Settings) -> Option<Check> {
|
||||
if name.to_lowercase() != name
|
||||
&& !settings
|
||||
.ignore_names
|
||||
.iter()
|
||||
.any(|ignore_name| ignore_name == name)
|
||||
{
|
||||
pub fn invalid_function_name(
|
||||
func_def: &Stmt,
|
||||
name: &str,
|
||||
ignore_names: &[String],
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Check> {
|
||||
if name.to_lowercase() != name && !ignore_names.iter().any(|ignore_name| ignore_name == name) {
|
||||
return Some(Check::new(
|
||||
CheckKind::InvalidFunctionName(name.to_string()),
|
||||
Range::from_located(func_def),
|
||||
identifier_range(func_def, locator),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// N803
|
||||
pub fn invalid_argument_name(name: &str, location: Range) -> Option<Check> {
|
||||
pub fn invalid_argument_name(name: &str, arg: &Arg) -> Option<Check> {
|
||||
if name.to_lowercase() != name {
|
||||
return Some(Check::new(
|
||||
CheckKind::InvalidArgumentName(name.to_string()),
|
||||
location,
|
||||
Range::from_located(arg),
|
||||
));
|
||||
}
|
||||
None
|
||||
@@ -124,7 +130,12 @@ pub fn invalid_first_argument_name_for_method(
|
||||
}
|
||||
|
||||
/// N807
|
||||
pub fn dunder_function_name(scope: &Scope, stmt: &Stmt, name: &str) -> Option<Check> {
|
||||
pub fn dunder_function_name(
|
||||
scope: &Scope,
|
||||
stmt: &Stmt,
|
||||
name: &str,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Check> {
|
||||
if matches!(scope.kind, ScopeKind::Class(_)) {
|
||||
return None;
|
||||
}
|
||||
@@ -138,7 +149,7 @@ pub fn dunder_function_name(scope: &Scope, stmt: &Stmt, name: &str) -> Option<Ch
|
||||
|
||||
Some(Check::new(
|
||||
CheckKind::DunderFunctionName,
|
||||
Range::from_located(stmt),
|
||||
identifier_range(stmt, locator),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -147,11 +158,12 @@ pub fn constant_imported_as_non_constant(
|
||||
import_from: &Stmt,
|
||||
name: &str,
|
||||
asname: &str,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Check> {
|
||||
if string::is_upper(name) && !string::is_upper(asname) {
|
||||
return Some(Check::new(
|
||||
CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()),
|
||||
Range::from_located(import_from),
|
||||
identifier_range(import_from, locator),
|
||||
));
|
||||
}
|
||||
None
|
||||
@@ -162,11 +174,12 @@ pub fn lowercase_imported_as_non_lowercase(
|
||||
import_from: &Stmt,
|
||||
name: &str,
|
||||
asname: &str,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Check> {
|
||||
if !string::is_upper(name) && string::is_lower(name) && asname.to_lowercase() != asname {
|
||||
return Some(Check::new(
|
||||
CheckKind::LowercaseImportedAsNonLowercase(name.to_string(), asname.to_string()),
|
||||
Range::from_located(import_from),
|
||||
identifier_range(import_from, locator),
|
||||
));
|
||||
}
|
||||
None
|
||||
@@ -177,11 +190,12 @@ pub fn camelcase_imported_as_lowercase(
|
||||
import_from: &Stmt,
|
||||
name: &str,
|
||||
asname: &str,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Check> {
|
||||
if helpers::is_camelcase(name) && string::is_lower(asname) {
|
||||
return Some(Check::new(
|
||||
CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()),
|
||||
Range::from_located(import_from),
|
||||
identifier_range(import_from, locator),
|
||||
));
|
||||
}
|
||||
None
|
||||
@@ -192,6 +206,7 @@ pub fn camelcase_imported_as_constant(
|
||||
import_from: &Stmt,
|
||||
name: &str,
|
||||
asname: &str,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Check> {
|
||||
if helpers::is_camelcase(name)
|
||||
&& !string::is_lower(asname)
|
||||
@@ -200,7 +215,7 @@ pub fn camelcase_imported_as_constant(
|
||||
{
|
||||
return Some(Check::new(
|
||||
CheckKind::CamelcaseImportedAsConstant(name.to_string(), asname.to_string()),
|
||||
Range::from_located(import_from),
|
||||
identifier_range(import_from, locator),
|
||||
));
|
||||
}
|
||||
None
|
||||
@@ -211,6 +226,7 @@ pub fn camelcase_imported_as_acronym(
|
||||
import_from: &Stmt,
|
||||
name: &str,
|
||||
asname: &str,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Check> {
|
||||
if helpers::is_camelcase(name)
|
||||
&& !string::is_lower(asname)
|
||||
@@ -219,7 +235,7 @@ pub fn camelcase_imported_as_acronym(
|
||||
{
|
||||
return Some(Check::new(
|
||||
CheckKind::CamelcaseImportedAsAcronym(name.to_string(), asname.to_string()),
|
||||
Range::from_located(import_from),
|
||||
identifier_range(import_from, locator),
|
||||
));
|
||||
}
|
||||
None
|
||||
|
||||
@@ -6,45 +6,45 @@ expression: checks
|
||||
InvalidClassName: bad
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
column: 6
|
||||
end_location:
|
||||
row: 2
|
||||
column: 8
|
||||
row: 1
|
||||
column: 9
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidClassName: _bad
|
||||
location:
|
||||
row: 5
|
||||
column: 0
|
||||
column: 6
|
||||
end_location:
|
||||
row: 6
|
||||
column: 8
|
||||
row: 5
|
||||
column: 10
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidClassName: bad_class
|
||||
location:
|
||||
row: 9
|
||||
column: 0
|
||||
column: 6
|
||||
end_location:
|
||||
row: 10
|
||||
column: 8
|
||||
row: 9
|
||||
column: 15
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidClassName: Bad_Class
|
||||
location:
|
||||
row: 13
|
||||
column: 0
|
||||
column: 6
|
||||
end_location:
|
||||
row: 14
|
||||
column: 8
|
||||
row: 13
|
||||
column: 15
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidClassName: BAD_CLASS
|
||||
location:
|
||||
row: 17
|
||||
column: 0
|
||||
column: 6
|
||||
end_location:
|
||||
row: 18
|
||||
column: 8
|
||||
row: 17
|
||||
column: 15
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -6,45 +6,45 @@ expression: checks
|
||||
InvalidFunctionName: Bad
|
||||
location:
|
||||
row: 4
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 5
|
||||
column: 8
|
||||
row: 4
|
||||
column: 7
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidFunctionName: _Bad
|
||||
location:
|
||||
row: 8
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 9
|
||||
row: 8
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidFunctionName: BAD
|
||||
location:
|
||||
row: 12
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 13
|
||||
column: 8
|
||||
row: 12
|
||||
column: 7
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidFunctionName: BAD_FUNC
|
||||
location:
|
||||
row: 16
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 17
|
||||
column: 8
|
||||
row: 16
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
InvalidFunctionName: testTest
|
||||
location:
|
||||
row: 40
|
||||
column: 4
|
||||
column: 8
|
||||
end_location:
|
||||
row: 41
|
||||
column: 19
|
||||
row: 40
|
||||
column: 16
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -5,17 +5,17 @@ expression: checks
|
||||
- kind: DunderFunctionName
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
column: 4
|
||||
end_location:
|
||||
row: 2
|
||||
column: 8
|
||||
row: 1
|
||||
column: 11
|
||||
fix: ~
|
||||
- kind: DunderFunctionName
|
||||
location:
|
||||
row: 14
|
||||
column: 4
|
||||
column: 8
|
||||
end_location:
|
||||
row: 15
|
||||
column: 12
|
||||
row: 14
|
||||
column: 15
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use itertools::izip;
|
||||
use rustpython_ast::{Location, Stmt, StmtKind};
|
||||
use rustpython_ast::{Located, Location, Stmt, StmtKind};
|
||||
use rustpython_parser::ast::{Cmpop, Expr, ExprKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
@@ -65,11 +65,11 @@ fn is_ambiguous_name(name: &str) -> bool {
|
||||
}
|
||||
|
||||
/// E741
|
||||
pub fn ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
|
||||
pub fn ambiguous_variable_name<T>(name: &str, located: &Located<T>) -> Option<Check> {
|
||||
if is_ambiguous_name(name) {
|
||||
Some(Check::new(
|
||||
CheckKind::AmbiguousVariableName(name.to_string()),
|
||||
location,
|
||||
Range::from_located(located),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -59,24 +59,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));
|
||||
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));
|
||||
bad_ops.insert(0, Cmpop::IsNot);
|
||||
}
|
||||
checks.push(check);
|
||||
@@ -90,23 +87,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));
|
||||
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));
|
||||
bad_ops.insert(0, Cmpop::IsNot);
|
||||
}
|
||||
checks.push(check);
|
||||
@@ -126,23 +121,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()) {
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
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),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
bad_ops.insert(idx, Cmpop::IsNot);
|
||||
}
|
||||
checks.push(check);
|
||||
@@ -156,23 +149,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));
|
||||
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),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
check.amend(Fix::dummy(expr.location));
|
||||
bad_ops.insert(idx, Cmpop::IsNot);
|
||||
}
|
||||
checks.push(check);
|
||||
@@ -190,9 +181,9 @@ pub fn literal_comparisons(
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(content) = compare(left, &ops, comparators) {
|
||||
if let Some(check) = checks.last_mut() {
|
||||
check.fix = Some(Fix::replacement(
|
||||
content,
|
||||
for check in &mut checks {
|
||||
check.amend(Fix::replacement(
|
||||
content.to_string(),
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
));
|
||||
|
||||
@@ -139,13 +139,13 @@ expression: checks
|
||||
row: 26
|
||||
column: 12
|
||||
fix:
|
||||
content: ""
|
||||
content: x is None is not None
|
||||
location:
|
||||
row: 26
|
||||
column: 3
|
||||
end_location:
|
||||
row: 26
|
||||
column: 3
|
||||
column: 20
|
||||
- kind:
|
||||
NoneComparison: NotEq
|
||||
location:
|
||||
|
||||
@@ -175,13 +175,13 @@ expression: checks
|
||||
row: 25
|
||||
column: 14
|
||||
fix:
|
||||
content: ""
|
||||
content: res is True is not False
|
||||
location:
|
||||
row: 25
|
||||
column: 3
|
||||
end_location:
|
||||
row: 25
|
||||
column: 3
|
||||
column: 23
|
||||
- kind:
|
||||
TrueFalseComparison:
|
||||
- false
|
||||
|
||||
214
src/resolver.rs
214
src/resolver.rs
@@ -3,21 +3,30 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use ignore::{DirEntry, WalkBuilder, WalkState};
|
||||
use log::debug;
|
||||
use path_absolutize::path_dedot;
|
||||
use rustc_hash::FxHashSet;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::cli::Overrides;
|
||||
use crate::fs;
|
||||
use crate::settings::configuration::Configuration;
|
||||
use crate::settings::pyproject::has_ruff_section;
|
||||
use crate::settings::{pyproject, Settings};
|
||||
|
||||
/// The strategy for discovering a `pyproject.toml` file for each Python file.
|
||||
/// The strategy used to discover Python files in the filesystem..
|
||||
#[derive(Debug)]
|
||||
pub enum Strategy {
|
||||
pub struct FileDiscovery {
|
||||
pub respect_gitignore: bool,
|
||||
}
|
||||
|
||||
/// The strategy used to discover the relevant `pyproject.toml` file for each
|
||||
/// Python file.
|
||||
#[derive(Debug)]
|
||||
pub enum PyprojectDiscovery {
|
||||
/// Use a fixed `pyproject.toml` file for all Python files (i.e., one
|
||||
/// provided on the command-line).
|
||||
Fixed(Settings),
|
||||
@@ -64,10 +73,10 @@ impl Resolver {
|
||||
}
|
||||
|
||||
/// Return the appropriate `Settings` for a given `Path`.
|
||||
pub fn resolve<'a>(&'a self, path: &Path, strategy: &'a Strategy) -> &'a Settings {
|
||||
pub fn resolve<'a>(&'a self, path: &Path, strategy: &'a PyprojectDiscovery) -> &'a Settings {
|
||||
match strategy {
|
||||
Strategy::Fixed(settings) => settings,
|
||||
Strategy::Hierarchical(default) => self
|
||||
PyprojectDiscovery::Fixed(settings) => settings,
|
||||
PyprojectDiscovery::Hierarchical(default) => self
|
||||
.settings
|
||||
.iter()
|
||||
.rev()
|
||||
@@ -164,99 +173,138 @@ fn is_excluded(file_path: &str, file_basename: &str, exclude: &globset::GlobSet)
|
||||
}
|
||||
|
||||
/// Return `true` if the `Path` appears to be that of a Python file.
|
||||
fn is_python_file(path: &Path) -> bool {
|
||||
fn is_python_path(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.map_or(false, |ext| ext == "py" || ext == "pyi")
|
||||
}
|
||||
|
||||
/// Find all Python (`.py` and `.pyi` files) in a set of `PathBuf`s.
|
||||
pub fn resolve_python_files(
|
||||
paths: &[PathBuf],
|
||||
strategy: &Strategy,
|
||||
overrides: &Overrides,
|
||||
) -> Result<(Vec<Result<DirEntry, walkdir::Error>>, Resolver)> {
|
||||
let mut files = Vec::new();
|
||||
let mut resolver = Resolver::default();
|
||||
for path in paths {
|
||||
let (files_in_path, file_resolver) = python_files_in_path(path, strategy, overrides)?;
|
||||
files.extend(files_in_path);
|
||||
resolver.merge(file_resolver);
|
||||
}
|
||||
Ok((files, resolver))
|
||||
/// Return `true` if the `Entry` appears to be that of a Python file.
|
||||
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 {
|
||||
|
||||
@@ -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>>,
|
||||
@@ -59,18 +60,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 +75,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,7 +106,15 @@ 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,
|
||||
@@ -129,10 +134,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),
|
||||
@@ -171,13 +189,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 +214,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 +229,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) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +43,7 @@ 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,
|
||||
@@ -95,26 +97,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 +129,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()]),
|
||||
@@ -183,6 +191,7 @@ 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,
|
||||
@@ -212,6 +221,7 @@ 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,
|
||||
@@ -296,26 +306,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 +347,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))?
|
||||
@@ -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,
|
||||
@@ -163,6 +172,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,
|
||||
@@ -209,6 +219,7 @@ exclude = ["foo.py"]
|
||||
format: None,
|
||||
unfixable: None,
|
||||
per_file_ignores: None,
|
||||
respect_gitignore: None,
|
||||
dummy_variable_rgx: None,
|
||||
src: None,
|
||||
target_version: None,
|
||||
@@ -251,6 +262,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,
|
||||
@@ -296,6 +308,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,
|
||||
@@ -350,7 +363,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 +396,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,
|
||||
|
||||
Reference in New Issue
Block a user