Compare commits
50 Commits
v0.1.8
...
SIM300-CON
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75564e8a4c | ||
|
|
0a0b0420ed | ||
|
|
6e3d5a78fd | ||
|
|
3f36780904 | ||
|
|
cbe3bf9bde | ||
|
|
4b4160eb48 | ||
|
|
29846f5b09 | ||
|
|
07b293d949 | ||
|
|
5ccc21aea2 | ||
|
|
f5d4019c2b | ||
|
|
bc0bf6f41c | ||
|
|
a2bc635584 | ||
|
|
09296e3e3c | ||
|
|
7c894921df | ||
|
|
be8f8e62b5 | ||
|
|
c97d3ddafb | ||
|
|
a7514295c1 | ||
|
|
0bf7683a3f | ||
|
|
c532089fb3 | ||
|
|
0977fa987b | ||
|
|
b4e101e157 | ||
|
|
2345cf1ec9 | ||
|
|
7489e6b881 | ||
|
|
02f8cc9ad3 | ||
|
|
0854f8cfa4 | ||
|
|
6feea863a6 | ||
|
|
2643f74a5d | ||
|
|
c944d23053 | ||
|
|
7879f9e921 | ||
|
|
935b77ec80 | ||
|
|
93d8c56d41 | ||
|
|
a336c1bc95 | ||
|
|
85b27a994f | ||
|
|
760ff26c06 | ||
|
|
0029b4fd07 | ||
|
|
2c6b534e1f | ||
|
|
6ecf844214 | ||
|
|
82731b8194 | ||
|
|
cd3c2f773f | ||
|
|
3ce145c476 | ||
|
|
db38078ca3 | ||
|
|
c8d6958d15 | ||
|
|
25b2361411 | ||
|
|
d1a7bc38ff | ||
|
|
6c224cec52 | ||
|
|
189e947808 | ||
|
|
28b1aa201b | ||
|
|
c99eae2c08 | ||
|
|
7256b882b9 | ||
|
|
b8fc006e52 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -226,7 +226,7 @@ jobs:
|
||||
name: ruff
|
||||
path: target/debug
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v2
|
||||
- uses: dawidd6/action-download-artifact@v3
|
||||
name: Download baseline Ruff binary
|
||||
with:
|
||||
name: ruff
|
||||
|
||||
4
.github/workflows/pr-comment.yaml
vendored
4
.github/workflows/pr-comment.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@v2
|
||||
- uses: dawidd6/action-download-artifact@v3
|
||||
name: Download pull request number
|
||||
with:
|
||||
name: pr-number
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "pr-number=$(<pr-number)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v2
|
||||
- uses: dawidd6/action-download-artifact@v3
|
||||
name: "Download ecosystem results"
|
||||
id: download-ecosystem-result
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### `site-packages` is now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))
|
||||
|
||||
Ruff maintains a list of default exclusions, which now consists of the following patterns:
|
||||
|
||||
- `.bzr`
|
||||
- `.direnv`
|
||||
- `.eggs`
|
||||
- `.git-rewrite`
|
||||
- `.git`
|
||||
- `.hg`
|
||||
- `.ipynb_checkpoints`
|
||||
- `.mypy_cache`
|
||||
- `.nox`
|
||||
- `.pants.d`
|
||||
- `.pyenv`
|
||||
- `.pytest_cache`
|
||||
- `.pytype`
|
||||
- `.ruff_cache`
|
||||
- `.svn`
|
||||
- `.tox`
|
||||
- `.venv`
|
||||
- `.vscode`
|
||||
- `__pypackages__`
|
||||
- `_build`
|
||||
- `buck-out`
|
||||
- `build`
|
||||
- `dist`
|
||||
- `node_modules`
|
||||
- `site-packages`
|
||||
- `venv`
|
||||
|
||||
Previously, the `site-packages` directory was not excluded by default. While `site-packages` tends
|
||||
to be excluded anyway by virtue of the `.venv` exclusion, this may not be the case when using Ruff
|
||||
from VS Code outside a virtual environment.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### The deprecated `format` setting has been removed
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
This release includes opt-in support for formatting Python snippets within
|
||||
docstrings via the `docstring-code-format` setting.
|
||||
[Check out the blog post](https://astral.sh/blog/v0.1.8) for more details!
|
||||
[Check out the blog post](https://astral.sh/blog/ruff-v0.1.8) for more details!
|
||||
|
||||
### Preview features
|
||||
|
||||
|
||||
@@ -556,10 +556,10 @@ examples.
|
||||
|
||||
#### Linux
|
||||
|
||||
Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf
|
||||
Install `perf` and build `ruff_benchmark` with the `profiling` profile and then run it with perf
|
||||
|
||||
```shell
|
||||
cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1
|
||||
cargo bench -p ruff_benchmark --no-run --profile=profiling && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=profiling -- --profile-time=1
|
||||
```
|
||||
|
||||
You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to
|
||||
@@ -567,8 +567,8 @@ gather enough samples for a good flamegraph (change the 999, the sample rate, an
|
||||
of checks, to your liking)
|
||||
|
||||
```shell
|
||||
cargo build --bin ruff_dev --profile=release-debug
|
||||
perf record -g -F 999 target/release-debug/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null
|
||||
cargo build --bin ruff_dev --profile=profiling
|
||||
perf record -g -F 999 target/profiling/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null
|
||||
```
|
||||
|
||||
Then convert the recorded profile
|
||||
@@ -598,7 +598,7 @@ cargo install cargo-instruments
|
||||
Then run the profiler with
|
||||
|
||||
```shell
|
||||
cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1
|
||||
cargo instruments -t time --bench linter --profile profiling -p ruff_benchmark -- --profile-time=1
|
||||
```
|
||||
|
||||
- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc`
|
||||
|
||||
65
Cargo.lock
generated
65
Cargo.lock
generated
@@ -828,7 +828,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"toml 0.7.8",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1482,9 +1482,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.18.0"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -1811,7 +1811,7 @@ dependencies = [
|
||||
"pep440_rs",
|
||||
"pep508_rs",
|
||||
"serde",
|
||||
"toml 0.8.2",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2155,7 +2155,7 @@ dependencies = [
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
"toml 0.7.8",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-indicatif",
|
||||
"tracing-subscriber",
|
||||
@@ -2255,7 +2255,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror",
|
||||
"toml 0.7.8",
|
||||
"toml",
|
||||
"typed-arena",
|
||||
"unicode-width",
|
||||
"unicode_names2",
|
||||
@@ -2543,7 +2543,7 @@ dependencies = [
|
||||
"shellexpand",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"toml 0.7.8",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3091,18 +3091,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.2"
|
||||
@@ -3112,7 +3100,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.20.2",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3124,19 +3112,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.20.2"
|
||||
@@ -3185,9 +3160,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-indicatif"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57e05fe4a1c906d94b275d8aeb8ff8b9deaca502aeb59ae8ab500a92b8032ac8"
|
||||
checksum = "069580424efe11d97c3fef4197fa98c004fa26672cc71ad8770d224e23b1951d"
|
||||
dependencies = [
|
||||
"indicatif",
|
||||
"tracing",
|
||||
@@ -3307,9 +3282,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode_names2"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d5506ae2c3c1ccbdf468e52fc5ef536c2ccd981f01273a4cb81aa61021f3a5f"
|
||||
checksum = "ac64ef2f016dc69dfa8283394a70b057066eb054d5fcb6b9eb17bd2ec5097211"
|
||||
dependencies = [
|
||||
"phf",
|
||||
"unicode_names2_generator",
|
||||
@@ -3317,9 +3292,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode_names2_generator"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6dfc680313e95bc6637fa278cd7a22390c3c2cd7b8b2bd28755bc6c0fc811e7"
|
||||
checksum = "013f6a731e80f3930de580e55ba41dfa846de4e0fdee4a701f97989cb1597d6a"
|
||||
dependencies = [
|
||||
"getopts",
|
||||
"log",
|
||||
@@ -3488,9 +3463,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.38"
|
||||
version = "0.4.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
|
||||
checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -3529,9 +3504,9 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.38"
|
||||
version = "0.3.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6433b7c56db97397842c46b67e11873eda263170afeb3a2dc74a7cb370fee0d"
|
||||
checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
@@ -3543,9 +3518,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.38"
|
||||
version = "0.3.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "493fcbab756bb764fa37e6bee8cec2dd709eb4273d06d0c282a5e74275ded735"
|
||||
checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
29
Cargo.toml
29
Cargo.toml
@@ -27,7 +27,7 @@ itertools = { version = "0.11.0" }
|
||||
libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
memchr = { version = "2.6.4" }
|
||||
once_cell = { version = "1.17.1" }
|
||||
once_cell = { version = "1.19.0" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
proc-macro2 = { version = "1.0.70" }
|
||||
quote = { version = "1.0.23" }
|
||||
@@ -45,12 +45,12 @@ strum_macros = { version = "0.25.3" }
|
||||
syn = { version = "2.0.40" }
|
||||
test-case = { version = "3.2.1" }
|
||||
thiserror = { version = "1.0.50" }
|
||||
toml = { version = "0.7.8" }
|
||||
toml = { version = "0.8.2" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-indicatif = { version = "0.3.4" }
|
||||
tracing-indicatif = { version = "0.3.6" }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
unicode-ident = { version = "1.0.12" }
|
||||
unicode_names2 = { version = "1.2.0" }
|
||||
unicode_names2 = { version = "1.2.1" }
|
||||
unicode-width = { version = "0.1.11" }
|
||||
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
|
||||
wsl = { version = "0.1.0" }
|
||||
@@ -88,7 +88,20 @@ rc_mutex = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
# Note that we set these explicitly, and these values
|
||||
# were chosen based on a trade-off between compile times
|
||||
# and runtime performance[1].
|
||||
#
|
||||
# [1]: https://github.com/astral-sh/ruff/pull/9031
|
||||
lto = "thin"
|
||||
codegen-units = 16
|
||||
|
||||
# Some crates don't change as much but benefit more from
|
||||
# more expensive optimization passes, so we selectively
|
||||
# decrease codegen-units in some cases.
|
||||
[profile.release.package.ruff_python_parser]
|
||||
codegen-units = 1
|
||||
[profile.release.package.ruff_python_ast]
|
||||
codegen-units = 1
|
||||
|
||||
[profile.dev.package.insta]
|
||||
@@ -102,8 +115,8 @@ opt-level = 3
|
||||
[profile.dev.package.ruff_python_parser]
|
||||
opt-level = 1
|
||||
|
||||
# Use the `--profile release-debug` flag to show symbols in release mode.
|
||||
# e.g. `cargo build --profile release-debug`
|
||||
[profile.release-debug]
|
||||
# Use the `--profile profiling` flag to show symbols in release mode.
|
||||
# e.g. `cargo build --profile profiling`
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
debug = 1
|
||||
|
||||
@@ -194,20 +194,25 @@ exclude = [
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
]
|
||||
|
||||
|
||||
@@ -515,7 +515,7 @@ impl<'a> FormatResults<'a> {
|
||||
if changed > 0 && unchanged > 0 {
|
||||
writeln!(
|
||||
f,
|
||||
"{} file{} {}, {} file{} left unchanged",
|
||||
"{} file{} {}, {} file{} {}",
|
||||
changed,
|
||||
if changed == 1 { "" } else { "s" },
|
||||
match self.mode {
|
||||
@@ -524,6 +524,10 @@ impl<'a> FormatResults<'a> {
|
||||
},
|
||||
unchanged,
|
||||
if unchanged == 1 { "" } else { "s" },
|
||||
match self.mode {
|
||||
FormatMode::Write => "left unchanged",
|
||||
FormatMode::Check | FormatMode::Diff => "already formatted",
|
||||
},
|
||||
)
|
||||
} else if changed > 0 {
|
||||
writeln!(
|
||||
@@ -539,9 +543,13 @@ impl<'a> FormatResults<'a> {
|
||||
} else if unchanged > 0 {
|
||||
writeln!(
|
||||
f,
|
||||
"{} file{} left unchanged",
|
||||
"{} file{} {}",
|
||||
unchanged,
|
||||
if unchanged == 1 { "" } else { "s" },
|
||||
match self.mode {
|
||||
FormatMode::Write => "left unchanged",
|
||||
FormatMode::Check | FormatMode::Diff => "already formatted",
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Ok(())
|
||||
|
||||
@@ -255,7 +255,7 @@ fn mixed_line_endings() -> Result<()> {
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
2 files left unchanged
|
||||
2 files already formatted
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
@@ -328,6 +328,60 @@ OTHER = "OTHER"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
fs::write(
|
||||
tempdir.path().join("main.py"),
|
||||
r#"
|
||||
from test import say_hy
|
||||
|
||||
if __name__ == "__main__":
|
||||
say_hy("dear Ruff contributor")
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(["format", "--no-cache", "--isolated", "--check"])
|
||||
.arg("main.py"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
Would reformat: main.py
|
||||
1 file would be reformatted
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(["format", "--no-cache", "--isolated"])
|
||||
.arg("main.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(["format", "--no-cache", "--isolated"])
|
||||
.arg("main.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file left unchanged
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_exclude() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
@@ -876,7 +930,7 @@ fn test_diff() {
|
||||
|
||||
|
||||
----- stderr -----
|
||||
2 files would be reformatted, 1 file left unchanged
|
||||
2 files would be reformatted, 1 file already formatted
|
||||
"###);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
/// A text edit to be applied to a source file. Inserts, deletes, or replaces
|
||||
/// content at a given location.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Edit {
|
||||
/// The start location of the edit.
|
||||
|
||||
@@ -182,3 +182,33 @@ class Foo(abc.ABC):
|
||||
return 1
|
||||
else:
|
||||
return 1.5
|
||||
|
||||
|
||||
def func(x: int):
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
return 2
|
||||
|
||||
|
||||
def func(x: int):
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
return 2
|
||||
else:
|
||||
return 3
|
||||
|
||||
|
||||
def func(x: int):
|
||||
if not x:
|
||||
raise ValueError
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
|
||||
def func(x: int):
|
||||
if not x:
|
||||
raise ValueError
|
||||
else:
|
||||
return 1
|
||||
|
||||
@@ -127,3 +127,21 @@ class MultipleConsecutiveFields(models.Model):
|
||||
pass
|
||||
|
||||
middle_name = models.CharField(max_length=32)
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
pass
|
||||
|
||||
|
||||
class StrBeforeFieldInheritedModel(BaseModel):
|
||||
"""Model with `__str__` before fields."""
|
||||
|
||||
class Meta:
|
||||
verbose_name = "test"
|
||||
verbose_name_plural = "tests"
|
||||
|
||||
def __str__(self):
|
||||
return "foobar"
|
||||
|
||||
first_name = models.CharField(max_length=32)
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import typing
|
||||
import typing_extensions
|
||||
from typing import TypeVar
|
||||
from typing_extensions import ParamSpec, TypeVarTuple
|
||||
|
||||
_T = typing.TypeVar("_T")
|
||||
_P = TypeVar("_P")
|
||||
_Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
_P = ParamSpec("_P")
|
||||
_P2 = typing.ParamSpec("_P2")
|
||||
_Ts2 = TypeVarTuple("_Ts2")
|
||||
|
||||
# OK
|
||||
_UsedTypeVar = TypeVar("_UsedTypeVar")
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import typing
|
||||
import typing_extensions
|
||||
from typing import TypeVar
|
||||
from typing_extensions import ParamSpec, TypeVarTuple
|
||||
|
||||
_T = typing.TypeVar("_T")
|
||||
_P = TypeVar("_P")
|
||||
_Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
_P = ParamSpec("_P")
|
||||
_P2 = typing.ParamSpec("_P2")
|
||||
_Ts2 = TypeVarTuple("_Ts2")
|
||||
|
||||
# OK
|
||||
_UsedTypeVar = TypeVar("_UsedTypeVar")
|
||||
|
||||
@@ -37,3 +37,28 @@ def func():
|
||||
|
||||
# PYI055
|
||||
x: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker
|
||||
|
||||
|
||||
def convert_union(union: UnionType) -> _T | None:
|
||||
converters: tuple[
|
||||
type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
] = union.__args__
|
||||
...
|
||||
|
||||
def convert_union(union: UnionType) -> _T | None:
|
||||
converters: tuple[
|
||||
Union[type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055
|
||||
] = union.__args__
|
||||
...
|
||||
|
||||
def convert_union(union: UnionType) -> _T | None:
|
||||
converters: tuple[
|
||||
Union[type[_T] | type[Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
] = union.__args__
|
||||
...
|
||||
|
||||
def convert_union(union: UnionType) -> _T | None:
|
||||
converters: tuple[
|
||||
Union[type[_T] | type[Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
] = union.__args__
|
||||
...
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Errors
|
||||
"yoda" == compare # SIM300
|
||||
"yoda" == compare # SIM300
|
||||
42 == age # SIM300
|
||||
("a", "b") == compare # SIM300
|
||||
"yoda" <= compare # SIM300
|
||||
@@ -13,10 +12,17 @@ YODA > age # SIM300
|
||||
YODA >= age # SIM300
|
||||
JediOrder.YODA == age # SIM300
|
||||
0 < (number - 100) # SIM300
|
||||
SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
B<A[0][0]or B
|
||||
B or(B)<A[0][0]
|
||||
|
||||
# Errors in preview
|
||||
['upper'] == UPPER_LIST
|
||||
{} == DummyHandler.CONFIG
|
||||
|
||||
# Errors in stable
|
||||
UPPER_LIST == ['upper']
|
||||
DummyHandler.CONFIG == {}
|
||||
|
||||
# OK
|
||||
compare == "yoda"
|
||||
age == 42
|
||||
@@ -31,3 +37,6 @@ age <= YODA
|
||||
YODA == YODA
|
||||
age == JediOrder.YODA
|
||||
(number - 100) > 0
|
||||
SECONDS_IN_DAY == 60 * 60 * 24 # Error in 0.1.8
|
||||
SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # Error in 0.1.8
|
||||
{"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
|
||||
|
||||
18
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH006_1.py
vendored
Normal file
18
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH006_1.py
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
x: "int" | str # TCH006
|
||||
x: ("int" | str) | "bool" # TCH006
|
||||
|
||||
|
||||
def func():
|
||||
x: "int" | str # OK
|
||||
|
||||
|
||||
z: list[str, str | "int"] = [] # TCH006
|
||||
|
||||
type A = Value["int" | str] # OK
|
||||
|
||||
OldS = TypeVar('OldS', int | 'str', str) # TCH006
|
||||
16
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH006_2.py
vendored
Normal file
16
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH006_2.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
x: "int" | str # TCH006
|
||||
x: ("int" | str) | "bool" # TCH006
|
||||
|
||||
|
||||
def func():
|
||||
x: "int" | str # OK
|
||||
|
||||
|
||||
z: list[str, str | "int"] = [] # TCH006
|
||||
|
||||
type A = Value["int" | str] # OK
|
||||
|
||||
OldS = TypeVar('OldS', int | 'str', str) # TCH006
|
||||
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_1.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_1.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Add `TYPE_CHECKING` to an existing `typing` import. Another member is moved."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
Const: Final[dict] = {}
|
||||
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_2.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_2.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Using `TYPE_CHECKING` from an existing `typing` import. Another member is moved."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final, TYPE_CHECKING
|
||||
|
||||
Const: Final[dict] = {}
|
||||
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_3.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_3.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Using `TYPE_CHECKING` from an existing `typing` import. Another member is moved."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final, Mapping
|
||||
|
||||
Const: Final[dict] = {}
|
||||
@@ -65,3 +65,28 @@ def f():
|
||||
|
||||
def func(value: DataFrame):
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
from pandas import DataFrame, Series
|
||||
|
||||
def baz() -> DataFrame | Series:
|
||||
...
|
||||
|
||||
|
||||
def f():
|
||||
from pandas import DataFrame, Series
|
||||
|
||||
def baz() -> (
|
||||
DataFrame |
|
||||
Series
|
||||
):
|
||||
...
|
||||
|
||||
class C:
|
||||
x: DataFrame[
|
||||
int
|
||||
] = 1
|
||||
|
||||
def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
|
||||
...
|
||||
|
||||
@@ -713,5 +713,12 @@ def retain_extra_whitespace_not_overindented():
|
||||
|
||||
This is not overindented
|
||||
This is overindented, but since one line is not overindented this should not raise
|
||||
And so is this, but it we should preserve the extra space on this line relative
|
||||
And so is this, but it we should preserve the extra space on this line relative
|
||||
"""
|
||||
|
||||
|
||||
def inconsistent_indent_byte_size():
|
||||
"""There's a non-breaking space (2-bytes) after 3 spaces (https://github.com/astral-sh/ruff/issues/9080).
|
||||
|
||||
Returns:
|
||||
"""
|
||||
|
||||
4
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_20.py
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_20.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import re
|
||||
from typing import Annotated
|
||||
|
||||
type X = Annotated[int, lambda: re.compile("x")]
|
||||
36
crates/ruff_linter/resources/test/fixtures/pylint/too_many_locals.py
vendored
Normal file
36
crates/ruff_linter/resources/test/fixtures/pylint/too_many_locals.py
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
def func() -> None: # OK
|
||||
# 15 is max default
|
||||
first = 1
|
||||
second = 2
|
||||
third = 3
|
||||
fourth = 4
|
||||
fifth = 5
|
||||
sixth = 6
|
||||
seventh = 7
|
||||
eighth = 8
|
||||
ninth = 9
|
||||
tenth = 10
|
||||
eleventh = 11
|
||||
twelveth = 12
|
||||
thirteenth = 13
|
||||
fourteenth = 14
|
||||
fifteenth = 15
|
||||
|
||||
|
||||
def func() -> None: # PLR0914
|
||||
first = 1
|
||||
second = 2
|
||||
third = 3
|
||||
fourth = 4
|
||||
fifth = 5
|
||||
sixth = 6
|
||||
seventh = 7
|
||||
eighth = 8
|
||||
ninth = 9
|
||||
tenth = 10
|
||||
eleventh = 11
|
||||
twelfth = 12
|
||||
thirteenth = 13
|
||||
fourteenth = 14
|
||||
fifteenth = 15
|
||||
sixteenth = 16
|
||||
61
crates/ruff_linter/resources/test/fixtures/refurb/FURB118.py
vendored
Normal file
61
crates/ruff_linter/resources/test/fixtures/refurb/FURB118.py
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# Errors.
|
||||
op_bitnot = lambda x: ~x
|
||||
op_not = lambda x: not x
|
||||
op_pos = lambda x: +x
|
||||
op_neg = lambda x: -x
|
||||
|
||||
op_add = lambda x, y: x + y
|
||||
op_sub = lambda x, y: x - y
|
||||
op_mult = lambda x, y: x * y
|
||||
op_matmutl = lambda x, y: x @ y
|
||||
op_truediv = lambda x, y: x / y
|
||||
op_mod = lambda x, y: x % y
|
||||
op_pow = lambda x, y: x ** y
|
||||
op_lshift = lambda x, y: x << y
|
||||
op_rshift = lambda x, y: x >> y
|
||||
op_bitor = lambda x, y: x | y
|
||||
op_xor = lambda x, y: x ^ y
|
||||
op_bitand = lambda x, y: x & y
|
||||
op_floordiv = lambda x, y: x // y
|
||||
|
||||
op_eq = lambda x, y: x == y
|
||||
op_ne = lambda x, y: x != y
|
||||
op_lt = lambda x, y: x < y
|
||||
op_lte = lambda x, y: x <= y
|
||||
op_gt = lambda x, y: x > y
|
||||
op_gte = lambda x, y: x >= y
|
||||
op_is = lambda x, y: x is y
|
||||
op_isnot = lambda x, y: x is not y
|
||||
op_in = lambda x, y: y in x
|
||||
|
||||
|
||||
def op_not2(x):
|
||||
return not x
|
||||
|
||||
|
||||
def op_add2(x, y):
|
||||
return x + y
|
||||
|
||||
|
||||
class Adder:
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
# OK.
|
||||
op_add3 = lambda x, y = 1: x + y
|
||||
op_neg2 = lambda x, y: y - x
|
||||
op_notin = lambda x, y: y not in x
|
||||
op_and = lambda x, y: y and x
|
||||
op_or = lambda x, y: y or x
|
||||
op_in = lambda x, y: x in y
|
||||
|
||||
|
||||
def op_neg3(x, y):
|
||||
return y - x
|
||||
|
||||
def op_add4(x, y = 1):
|
||||
return x + y
|
||||
|
||||
def op_add5(x, y):
|
||||
print("op_add5")
|
||||
return x + y
|
||||
@@ -122,3 +122,33 @@ async def f():
|
||||
# OK
|
||||
async def f():
|
||||
task[i] = asyncio.create_task(coordinator.ws_connect())
|
||||
|
||||
|
||||
# OK
|
||||
async def f(x: int):
|
||||
if x > 0:
|
||||
task = asyncio.create_task(make_request())
|
||||
else:
|
||||
task = asyncio.create_task(make_request())
|
||||
await task
|
||||
|
||||
|
||||
# OK
|
||||
async def f(x: bool):
|
||||
if x:
|
||||
t = asyncio.create_task(asyncio.sleep(1))
|
||||
else:
|
||||
t = None
|
||||
try:
|
||||
await asyncio.sleep(1)
|
||||
finally:
|
||||
if t:
|
||||
await t
|
||||
|
||||
|
||||
# Error
|
||||
async def f(x: bool):
|
||||
if x:
|
||||
t = asyncio.create_task(asyncio.sleep(1))
|
||||
else:
|
||||
t = None
|
||||
|
||||
@@ -59,3 +59,11 @@ class F(BaseSettings):
|
||||
without_annotation = []
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
final_variable: Final[list[int]] = []
|
||||
|
||||
|
||||
class G(F):
|
||||
mutable_default: list[int] = []
|
||||
immutable_annotation: Sequence[int] = []
|
||||
without_annotation = []
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
final_variable: Final[list[int]] = []
|
||||
|
||||
@@ -3,12 +3,11 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint, ruff};
|
||||
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint};
|
||||
|
||||
/// Run lint rules over the [`Binding`]s.
|
||||
pub(crate) fn bindings(checker: &mut Checker) {
|
||||
if !checker.any_enabled(&[
|
||||
Rule::AsyncioDanglingTask,
|
||||
Rule::InvalidAllFormat,
|
||||
Rule::InvalidAllObject,
|
||||
Rule::NonAsciiName,
|
||||
@@ -72,12 +71,5 @@ pub(crate) fn bindings(checker: &mut Checker) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::AsyncioDanglingTask) {
|
||||
if let Some(diagnostic) =
|
||||
ruff::rules::asyncio_dangling_binding(binding, &checker.semantic)
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use ruff_python_ast::Expr;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_pie, pylint};
|
||||
use crate::rules::{flake8_pie, pylint, refurb};
|
||||
|
||||
/// Run lint rules over all deferred lambdas in the [`SemanticModel`].
|
||||
pub(crate) fn deferred_lambdas(checker: &mut Checker) {
|
||||
@@ -21,6 +21,9 @@ pub(crate) fn deferred_lambdas(checker: &mut Checker) {
|
||||
if checker.enabled(Rule::ReimplementedContainerBuiltin) {
|
||||
flake8_pie::rules::reimplemented_container_builtin(checker, lambda);
|
||||
}
|
||||
if checker.enabled(Rule::ReimplementedOperator) {
|
||||
refurb::rules::reimplemented_operator(checker, &lambda.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,21 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint};
|
||||
use crate::rules::{
|
||||
flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint, ruff,
|
||||
};
|
||||
|
||||
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
|
||||
pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
if !checker.any_enabled(&[
|
||||
Rule::AsyncioDanglingTask,
|
||||
Rule::GlobalVariableNotAssigned,
|
||||
Rule::ImportShadowedByLoopVar,
|
||||
Rule::NoSelfUse,
|
||||
Rule::RedefinedArgumentFromLocal,
|
||||
Rule::RedefinedWhileUnused,
|
||||
Rule::RuntimeImportInTypeCheckingBlock,
|
||||
Rule::TooManyLocals,
|
||||
Rule::TypingOnlyFirstPartyImport,
|
||||
Rule::TypingOnlyStandardLibraryImport,
|
||||
Rule::TypingOnlyThirdPartyImport,
|
||||
@@ -31,7 +36,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
Rule::UnusedPrivateTypedDict,
|
||||
Rule::UnusedStaticMethodArgument,
|
||||
Rule::UnusedVariable,
|
||||
Rule::NoSelfUse,
|
||||
]) {
|
||||
return;
|
||||
}
|
||||
@@ -269,6 +273,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::AsyncioDanglingTask) {
|
||||
ruff::rules::asyncio_dangling_binding(scope, &checker.semantic, &mut diagnostics);
|
||||
}
|
||||
|
||||
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
|
||||
if checker.enabled(Rule::UnusedVariable) {
|
||||
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
|
||||
@@ -336,6 +344,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
if checker.enabled(Rule::NoSelfUse) {
|
||||
pylint::rules::no_self_use(checker, scope_id, scope, &mut diagnostics);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::TooManyLocals) {
|
||||
pylint::rules::too_many_locals(checker, scope, &mut diagnostics);
|
||||
}
|
||||
}
|
||||
}
|
||||
checker.diagnostics.extend(diagnostics);
|
||||
|
||||
@@ -15,8 +15,9 @@ use crate::rules::{
|
||||
flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
|
||||
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging,
|
||||
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self,
|
||||
flake8_simplify, flake8_tidy_imports, flake8_trio, flake8_use_pathlib, flynt, numpy,
|
||||
pandas_vet, pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff,
|
||||
flake8_simplify, flake8_tidy_imports, flake8_trio, flake8_type_checking, flake8_use_pathlib,
|
||||
flynt, numpy, pandas_vet, pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade,
|
||||
refurb, ruff,
|
||||
};
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
@@ -1170,6 +1171,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryTypeUnion) {
|
||||
flake8_pyi::rules::unnecessary_type_union(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::RuntimeStringUnion) {
|
||||
flake8_type_checking::rules::runtime_string_union(checker, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::UnaryOp(
|
||||
|
||||
@@ -368,6 +368,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
.diagnostics
|
||||
.extend(ruff::rules::unreachable::in_function(name, body));
|
||||
}
|
||||
if checker.enabled(Rule::ReimplementedOperator) {
|
||||
refurb::rules::reimplemented_operator(checker, &function_def.into());
|
||||
}
|
||||
}
|
||||
Stmt::Return(_) => {
|
||||
if checker.enabled(Rule::ReturnOutsideFunction) {
|
||||
@@ -397,27 +400,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
flake8_django::rules::nullable_model_string_field(checker, body);
|
||||
}
|
||||
if checker.enabled(Rule::DjangoExcludeWithModelForm) {
|
||||
if let Some(diagnostic) = flake8_django::rules::exclude_with_model_form(
|
||||
checker,
|
||||
arguments.as_deref(),
|
||||
body,
|
||||
) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
flake8_django::rules::exclude_with_model_form(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::DjangoAllWithModelForm) {
|
||||
if let Some(diagnostic) =
|
||||
flake8_django::rules::all_with_model_form(checker, arguments.as_deref(), body)
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
flake8_django::rules::all_with_model_form(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::DjangoUnorderedBodyContentInModel) {
|
||||
flake8_django::rules::unordered_body_content_in_model(
|
||||
checker,
|
||||
arguments.as_deref(),
|
||||
body,
|
||||
);
|
||||
flake8_django::rules::unordered_body_content_in_model(checker, class_def);
|
||||
}
|
||||
if !checker.source_type.is_stub() {
|
||||
if checker.enabled(Rule::DjangoModelWithoutDunderStr) {
|
||||
|
||||
@@ -2013,13 +2013,15 @@ pub(crate) fn check_ast(
|
||||
// Iterate over the AST.
|
||||
checker.visit_body(python_ast);
|
||||
|
||||
// Visit any deferred syntax nodes.
|
||||
// Visit any deferred syntax nodes. Take care to visit in order, such that we avoid adding
|
||||
// new deferred nodes after visiting nodes of that kind. For example, visiting a deferred
|
||||
// function can add a deferred lambda, but the opposite is not true.
|
||||
checker.visit_deferred_functions();
|
||||
checker.visit_deferred_lambdas();
|
||||
checker.visit_deferred_future_type_definitions();
|
||||
checker.visit_deferred_type_param_definitions();
|
||||
checker.visit_deferred_future_type_definitions();
|
||||
let allocator = typed_arena::Arena::new();
|
||||
checker.visit_deferred_string_type_definitions(&allocator);
|
||||
checker.visit_deferred_lambdas();
|
||||
checker.visit_exports();
|
||||
|
||||
// Check docstrings, bindings, and unresolved references.
|
||||
|
||||
@@ -252,6 +252,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R0911") => (RuleGroup::Stable, rules::pylint::rules::TooManyReturnStatements),
|
||||
(Pylint, "R0912") => (RuleGroup::Stable, rules::pylint::rules::TooManyBranches),
|
||||
(Pylint, "R0913") => (RuleGroup::Stable, rules::pylint::rules::TooManyArguments),
|
||||
(Pylint, "R0914") => (RuleGroup::Preview, rules::pylint::rules::TooManyLocals),
|
||||
(Pylint, "R0915") => (RuleGroup::Stable, rules::pylint::rules::TooManyStatements),
|
||||
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
|
||||
(Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositional),
|
||||
@@ -807,6 +808,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8TypeChecking, "003") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport),
|
||||
(Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
|
||||
(Flake8TypeChecking, "005") => (RuleGroup::Stable, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
|
||||
(Flake8TypeChecking, "006") => (RuleGroup::Preview, rules::flake8_type_checking::rules::RuntimeStringUnion),
|
||||
|
||||
// tryceratops
|
||||
(Tryceratops, "002") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseVanillaClass),
|
||||
@@ -951,6 +953,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "105") => (RuleGroup::Preview, rules::refurb::rules::PrintEmptyString),
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
|
||||
(Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator),
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
|
||||
#[allow(deprecated)]
|
||||
|
||||
@@ -13,7 +13,7 @@ use ruff_text_size::{Ranged, TextSize};
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::imports::{AnyImport, Import, ImportFrom};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::{ImportedName, SemanticModel};
|
||||
use ruff_python_trivia::textwrap::indent;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
@@ -132,7 +132,48 @@ impl<'a> Importer<'a> {
|
||||
)?;
|
||||
|
||||
// Import the `TYPE_CHECKING` symbol from the typing module.
|
||||
let (type_checking_edit, type_checking) = self.get_or_import_type_checking(at, semantic)?;
|
||||
let (type_checking_edit, type_checking) =
|
||||
if let Some(type_checking) = Self::find_type_checking(at, semantic)? {
|
||||
// Special-case: if the `TYPE_CHECKING` symbol is imported as part of the same
|
||||
// statement that we're modifying, avoid adding a no-op edit. For example, here,
|
||||
// the `TYPE_CHECKING` no-op edit would overlap with the edit to remove `Final`
|
||||
// from the import:
|
||||
// ```python
|
||||
// from __future__ import annotations
|
||||
//
|
||||
// from typing import Final, TYPE_CHECKING
|
||||
//
|
||||
// Const: Final[dict] = {}
|
||||
// ```
|
||||
let edit = if type_checking.statement(semantic) == import.statement {
|
||||
None
|
||||
} else {
|
||||
Some(Edit::range_replacement(
|
||||
self.locator.slice(type_checking.range()).to_string(),
|
||||
type_checking.range(),
|
||||
))
|
||||
};
|
||||
(edit, type_checking.into_name())
|
||||
} else {
|
||||
// Special-case: if the `TYPE_CHECKING` symbol would be added to the same import
|
||||
// we're modifying, import it as a separate import statement. For example, here,
|
||||
// we're concurrently removing `Final` and adding `TYPE_CHECKING`, so it's easier to
|
||||
// use a separate import statement:
|
||||
// ```python
|
||||
// from __future__ import annotations
|
||||
//
|
||||
// from typing import Final
|
||||
//
|
||||
// Const: Final[dict] = {}
|
||||
// ```
|
||||
let (edit, name) = self.import_symbol(
|
||||
&ImportRequest::import_from("typing", "TYPE_CHECKING"),
|
||||
at,
|
||||
Some(import.statement),
|
||||
semantic,
|
||||
)?;
|
||||
(Some(edit), name)
|
||||
};
|
||||
|
||||
// Add the import to a `TYPE_CHECKING` block.
|
||||
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
|
||||
@@ -157,28 +198,21 @@ impl<'a> Importer<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate an [`Edit`] to reference `typing.TYPE_CHECKING`. Returns the [`Edit`] necessary to
|
||||
/// make the symbol available in the current scope along with the bound name of the symbol.
|
||||
fn get_or_import_type_checking(
|
||||
&self,
|
||||
/// Find a reference to `typing.TYPE_CHECKING`.
|
||||
fn find_type_checking(
|
||||
at: TextSize,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<(Edit, String), ResolutionError> {
|
||||
) -> Result<Option<ImportedName>, ResolutionError> {
|
||||
for module in semantic.typing_modules() {
|
||||
if let Some((edit, name)) = self.get_symbol(
|
||||
if let Some(imported_name) = Self::find_symbol(
|
||||
&ImportRequest::import_from(module, "TYPE_CHECKING"),
|
||||
at,
|
||||
semantic,
|
||||
)? {
|
||||
return Ok((edit, name));
|
||||
return Ok(Some(imported_name));
|
||||
}
|
||||
}
|
||||
|
||||
self.import_symbol(
|
||||
&ImportRequest::import_from("typing", "TYPE_CHECKING"),
|
||||
at,
|
||||
semantic,
|
||||
)
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
|
||||
@@ -192,16 +226,15 @@ impl<'a> Importer<'a> {
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<(Edit, String), ResolutionError> {
|
||||
self.get_symbol(symbol, at, semantic)?
|
||||
.map_or_else(|| self.import_symbol(symbol, at, semantic), Ok)
|
||||
.map_or_else(|| self.import_symbol(symbol, at, None, semantic), Ok)
|
||||
}
|
||||
|
||||
/// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
|
||||
fn get_symbol(
|
||||
&self,
|
||||
/// Return the [`ImportedName`] to for existing symbol, if it's present in the given [`SemanticModel`].
|
||||
fn find_symbol(
|
||||
symbol: &ImportRequest,
|
||||
at: TextSize,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<Option<(Edit, String)>, ResolutionError> {
|
||||
) -> Result<Option<ImportedName>, ResolutionError> {
|
||||
// If the symbol is already available in the current scope, use it.
|
||||
let Some(imported_name) =
|
||||
semantic.resolve_qualified_import_name(symbol.module, symbol.member)
|
||||
@@ -226,6 +259,21 @@ impl<'a> Importer<'a> {
|
||||
return Err(ResolutionError::IncompatibleContext);
|
||||
}
|
||||
|
||||
Ok(Some(imported_name))
|
||||
}
|
||||
|
||||
/// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
|
||||
fn get_symbol(
|
||||
&self,
|
||||
symbol: &ImportRequest,
|
||||
at: TextSize,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<Option<(Edit, String)>, ResolutionError> {
|
||||
// Find the symbol in the current scope.
|
||||
let Some(imported_name) = Self::find_symbol(symbol, at, semantic)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// We also add a no-op edit to force conflicts with any other fixes that might try to
|
||||
// remove the import. Consider:
|
||||
//
|
||||
@@ -259,9 +307,13 @@ impl<'a> Importer<'a> {
|
||||
&self,
|
||||
symbol: &ImportRequest,
|
||||
at: TextSize,
|
||||
except: Option<&Stmt>,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<(Edit, String), ResolutionError> {
|
||||
if let Some(stmt) = self.find_import_from(symbol.module, at) {
|
||||
if let Some(stmt) = self
|
||||
.find_import_from(symbol.module, at)
|
||||
.filter(|stmt| except != Some(stmt))
|
||||
{
|
||||
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
|
||||
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
|
||||
// bound name.
|
||||
@@ -423,14 +475,18 @@ impl RuntimeImportEdit {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TypingImportEdit {
|
||||
/// The edit to add the `TYPE_CHECKING` symbol to the module.
|
||||
type_checking_edit: Edit,
|
||||
type_checking_edit: Option<Edit>,
|
||||
/// The edit to add the import to a `TYPE_CHECKING` block.
|
||||
add_import_edit: Edit,
|
||||
}
|
||||
|
||||
impl TypingImportEdit {
|
||||
pub(crate) fn into_edits(self) -> Vec<Edit> {
|
||||
vec![self.type_checking_edit, self.add_import_edit]
|
||||
pub(crate) fn into_edits(self) -> (Edit, Option<Edit>) {
|
||||
if let Some(type_checking_edit) = self.type_checking_edit {
|
||||
(type_checking_edit, Some(self.add_import_edit))
|
||||
} else {
|
||||
(self.add_import_edit, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::helpers::{
|
||||
implicit_return, pep_604_union, typing_optional, typing_union, ReturnStatementVisitor,
|
||||
pep_604_union, typing_optional, typing_union, ReturnStatementVisitor, Terminal,
|
||||
};
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr, ExprContext};
|
||||
@@ -57,6 +57,14 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
|
||||
visitor.returns
|
||||
};
|
||||
|
||||
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
|
||||
let terminal = Terminal::from_function(function);
|
||||
|
||||
// If every control flow path raises an exception, return `NoReturn`.
|
||||
if terminal == Some(Terminal::Raise) {
|
||||
return Some(AutoPythonType::Never);
|
||||
}
|
||||
|
||||
// Determine the return type of the first `return` statement.
|
||||
let Some((return_statement, returns)) = returns.split_first() else {
|
||||
return Some(AutoPythonType::Atom(PythonType::None));
|
||||
@@ -80,7 +88,7 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
|
||||
// if x > 0:
|
||||
// return 1
|
||||
// ```
|
||||
if implicit_return(function) {
|
||||
if terminal.is_none() {
|
||||
return_type = return_type.union(ResolvedPythonType::Atom(PythonType::None));
|
||||
}
|
||||
|
||||
@@ -94,6 +102,7 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AutoPythonType {
|
||||
Never,
|
||||
Atom(PythonType),
|
||||
Union(FxHashSet<PythonType>),
|
||||
}
|
||||
@@ -111,6 +120,28 @@ impl AutoPythonType {
|
||||
target_version: PythonVersion,
|
||||
) -> Option<(Expr, Vec<Edit>)> {
|
||||
match self {
|
||||
AutoPythonType::Never => {
|
||||
let (no_return_edit, binding) = importer
|
||||
.get_or_import_symbol(
|
||||
&ImportRequest::import_from(
|
||||
"typing",
|
||||
if target_version >= PythonVersion::Py311 {
|
||||
"Never"
|
||||
} else {
|
||||
"NoReturn"
|
||||
},
|
||||
),
|
||||
at,
|
||||
semantic,
|
||||
)
|
||||
.ok()?;
|
||||
let expr = Expr::Name(ast::ExprName {
|
||||
id: binding,
|
||||
range: TextRange::default(),
|
||||
ctx: ExprContext::Load,
|
||||
});
|
||||
Some((expr, vec![no_return_edit]))
|
||||
}
|
||||
AutoPythonType::Atom(python_type) => {
|
||||
let expr = type_expr(python_type)?;
|
||||
Some((expr, vec![]))
|
||||
|
||||
@@ -495,4 +495,88 @@ auto_return_type.py:180:9: ANN201 [*] Missing return type annotation for public
|
||||
182 182 | return 1
|
||||
183 183 | else:
|
||||
|
||||
auto_return_type.py:187:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
187 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
188 | try:
|
||||
189 | pass
|
||||
|
|
||||
= help: Add return type annotation: `int | None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
184 184 | return 1.5
|
||||
185 185 |
|
||||
186 186 |
|
||||
187 |-def func(x: int):
|
||||
187 |+def func(x: int) -> int | None:
|
||||
188 188 | try:
|
||||
189 189 | pass
|
||||
190 190 | except:
|
||||
|
||||
auto_return_type.py:194:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
194 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
195 | try:
|
||||
196 | pass
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
191 191 | return 2
|
||||
192 192 |
|
||||
193 193 |
|
||||
194 |-def func(x: int):
|
||||
194 |+def func(x: int) -> int:
|
||||
195 195 | try:
|
||||
196 196 | pass
|
||||
197 197 | except:
|
||||
|
||||
auto_return_type.py:203:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
203 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
204 | if not x:
|
||||
205 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `Never`
|
||||
|
||||
ℹ Unsafe fix
|
||||
151 151 |
|
||||
152 152 | import abc
|
||||
153 153 | from abc import abstractmethod
|
||||
154 |+from typing import Never
|
||||
154 155 |
|
||||
155 156 |
|
||||
156 157 | class Foo(abc.ABC):
|
||||
--------------------------------------------------------------------------------
|
||||
200 201 | return 3
|
||||
201 202 |
|
||||
202 203 |
|
||||
203 |-def func(x: int):
|
||||
204 |+def func(x: int) -> Never:
|
||||
204 205 | if not x:
|
||||
205 206 | raise ValueError
|
||||
206 207 | else:
|
||||
|
||||
auto_return_type.py:210:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
210 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
211 | if not x:
|
||||
212 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
207 207 | raise TypeError
|
||||
208 208 |
|
||||
209 209 |
|
||||
210 |-def func(x: int):
|
||||
210 |+def func(x: int) -> int:
|
||||
211 211 | if not x:
|
||||
212 212 | raise ValueError
|
||||
213 213 | else:
|
||||
|
||||
|
||||
|
||||
@@ -550,4 +550,96 @@ auto_return_type.py:180:9: ANN201 [*] Missing return type annotation for public
|
||||
182 182 | return 1
|
||||
183 183 | else:
|
||||
|
||||
auto_return_type.py:187:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
187 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
188 | try:
|
||||
189 | pass
|
||||
|
|
||||
= help: Add return type annotation: `Optional[int]`
|
||||
|
||||
ℹ Unsafe fix
|
||||
151 151 |
|
||||
152 152 | import abc
|
||||
153 153 | from abc import abstractmethod
|
||||
154 |+from typing import Optional
|
||||
154 155 |
|
||||
155 156 |
|
||||
156 157 | class Foo(abc.ABC):
|
||||
--------------------------------------------------------------------------------
|
||||
184 185 | return 1.5
|
||||
185 186 |
|
||||
186 187 |
|
||||
187 |-def func(x: int):
|
||||
188 |+def func(x: int) -> Optional[int]:
|
||||
188 189 | try:
|
||||
189 190 | pass
|
||||
190 191 | except:
|
||||
|
||||
auto_return_type.py:194:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
194 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
195 | try:
|
||||
196 | pass
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
191 191 | return 2
|
||||
192 192 |
|
||||
193 193 |
|
||||
194 |-def func(x: int):
|
||||
194 |+def func(x: int) -> int:
|
||||
195 195 | try:
|
||||
196 196 | pass
|
||||
197 197 | except:
|
||||
|
||||
auto_return_type.py:203:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
203 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
204 | if not x:
|
||||
205 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `NoReturn`
|
||||
|
||||
ℹ Unsafe fix
|
||||
151 151 |
|
||||
152 152 | import abc
|
||||
153 153 | from abc import abstractmethod
|
||||
154 |+from typing import NoReturn
|
||||
154 155 |
|
||||
155 156 |
|
||||
156 157 | class Foo(abc.ABC):
|
||||
--------------------------------------------------------------------------------
|
||||
200 201 | return 3
|
||||
201 202 |
|
||||
202 203 |
|
||||
203 |-def func(x: int):
|
||||
204 |+def func(x: int) -> NoReturn:
|
||||
204 205 | if not x:
|
||||
205 206 | raise ValueError
|
||||
206 207 | else:
|
||||
|
||||
auto_return_type.py:210:5: ANN201 [*] Missing return type annotation for public function `func`
|
||||
|
|
||||
210 | def func(x: int):
|
||||
| ^^^^ ANN201
|
||||
211 | if not x:
|
||||
212 | raise ValueError
|
||||
|
|
||||
= help: Add return type annotation: `int`
|
||||
|
||||
ℹ Unsafe fix
|
||||
207 207 | raise TypeError
|
||||
208 208 |
|
||||
209 209 |
|
||||
210 |-def func(x: int):
|
||||
210 |+def func(x: int) -> int:
|
||||
211 211 | if not x:
|
||||
212 212 | raise ValueError
|
||||
213 213 | else:
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr};
|
||||
@@ -6,6 +6,7 @@ use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::add_argument;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `zip` calls without an explicit `strict` parameter.
|
||||
@@ -28,16 +29,25 @@ use crate::checkers::ast::Checker;
|
||||
/// zip(a, b, strict=True)
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe for `zip` calls that contain
|
||||
/// `**kwargs`, as adding a `check` keyword argument to such a call may lead
|
||||
/// to a duplicate keyword argument error.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `zip`](https://docs.python.org/3/library/functions.html#zip)
|
||||
#[violation]
|
||||
pub struct ZipWithoutExplicitStrict;
|
||||
|
||||
impl Violation for ZipWithoutExplicitStrict {
|
||||
impl AlwaysFixableViolation for ZipWithoutExplicitStrict {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`zip()` without an explicit `strict=` parameter")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Add explicit `strict=False`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// B905
|
||||
@@ -52,9 +62,27 @@ pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::Exp
|
||||
.iter()
|
||||
.any(|arg| is_infinite_iterator(arg, checker.semantic()))
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(ZipWithoutExplicitStrict, call.range()));
|
||||
let mut diagnostic = Diagnostic::new(ZipWithoutExplicitStrict, call.range());
|
||||
diagnostic.set_fix(Fix::applicable_edit(
|
||||
add_argument(
|
||||
"strict=False",
|
||||
&call.arguments,
|
||||
checker.indexer().comment_ranges(),
|
||||
checker.locator().contents(),
|
||||
),
|
||||
// If the function call contains `**kwargs`, mark the fix as unsafe.
|
||||
if call
|
||||
.arguments
|
||||
.keywords
|
||||
.iter()
|
||||
.any(|keyword| keyword.arg.is_none())
|
||||
{
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
},
|
||||
));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B905.py:4:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
B905.py:4:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
3 | # Errors
|
||||
4 | zip()
|
||||
@@ -9,8 +9,19 @@ B905.py:4:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
5 | zip(range(3))
|
||||
6 | zip("a", "b")
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:5:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
1 1 | from itertools import count, cycle, repeat
|
||||
2 2 |
|
||||
3 3 | # Errors
|
||||
4 |-zip()
|
||||
4 |+zip(strict=False)
|
||||
5 5 | zip(range(3))
|
||||
6 6 | zip("a", "b")
|
||||
7 7 | zip("a", "b", *zip("c"))
|
||||
|
||||
B905.py:5:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
3 | # Errors
|
||||
4 | zip()
|
||||
@@ -19,8 +30,19 @@ B905.py:5:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
6 | zip("a", "b")
|
||||
7 | zip("a", "b", *zip("c"))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:6:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
2 2 |
|
||||
3 3 | # Errors
|
||||
4 4 | zip()
|
||||
5 |-zip(range(3))
|
||||
5 |+zip(range(3), strict=False)
|
||||
6 6 | zip("a", "b")
|
||||
7 7 | zip("a", "b", *zip("c"))
|
||||
8 8 | zip(zip("a"), strict=False)
|
||||
|
||||
B905.py:6:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
4 | zip()
|
||||
5 | zip(range(3))
|
||||
@@ -29,8 +51,19 @@ B905.py:6:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
7 | zip("a", "b", *zip("c"))
|
||||
8 | zip(zip("a"), strict=False)
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:7:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
3 3 | # Errors
|
||||
4 4 | zip()
|
||||
5 5 | zip(range(3))
|
||||
6 |-zip("a", "b")
|
||||
6 |+zip("a", "b", strict=False)
|
||||
7 7 | zip("a", "b", *zip("c"))
|
||||
8 8 | zip(zip("a"), strict=False)
|
||||
9 9 | zip(zip("a", strict=True))
|
||||
|
||||
B905.py:7:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
5 | zip(range(3))
|
||||
6 | zip("a", "b")
|
||||
@@ -39,8 +72,19 @@ B905.py:7:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
8 | zip(zip("a"), strict=False)
|
||||
9 | zip(zip("a", strict=True))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:7:16: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
4 4 | zip()
|
||||
5 5 | zip(range(3))
|
||||
6 6 | zip("a", "b")
|
||||
7 |-zip("a", "b", *zip("c"))
|
||||
7 |+zip("a", "b", *zip("c"), strict=False)
|
||||
8 8 | zip(zip("a"), strict=False)
|
||||
9 9 | zip(zip("a", strict=True))
|
||||
10 10 |
|
||||
|
||||
B905.py:7:16: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
5 | zip(range(3))
|
||||
6 | zip("a", "b")
|
||||
@@ -49,8 +93,19 @@ B905.py:7:16: B905 `zip()` without an explicit `strict=` parameter
|
||||
8 | zip(zip("a"), strict=False)
|
||||
9 | zip(zip("a", strict=True))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:8:5: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
4 4 | zip()
|
||||
5 5 | zip(range(3))
|
||||
6 6 | zip("a", "b")
|
||||
7 |-zip("a", "b", *zip("c"))
|
||||
7 |+zip("a", "b", *zip("c", strict=False))
|
||||
8 8 | zip(zip("a"), strict=False)
|
||||
9 9 | zip(zip("a", strict=True))
|
||||
10 10 |
|
||||
|
||||
B905.py:8:5: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
6 | zip("a", "b")
|
||||
7 | zip("a", "b", *zip("c"))
|
||||
@@ -58,8 +113,19 @@ B905.py:8:5: B905 `zip()` without an explicit `strict=` parameter
|
||||
| ^^^^^^^^ B905
|
||||
9 | zip(zip("a", strict=True))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:9:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
5 5 | zip(range(3))
|
||||
6 6 | zip("a", "b")
|
||||
7 7 | zip("a", "b", *zip("c"))
|
||||
8 |-zip(zip("a"), strict=False)
|
||||
8 |+zip(zip("a", strict=False), strict=False)
|
||||
9 9 | zip(zip("a", strict=True))
|
||||
10 10 |
|
||||
11 11 | # OK
|
||||
|
||||
B905.py:9:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
7 | zip("a", "b", *zip("c"))
|
||||
8 | zip(zip("a"), strict=False)
|
||||
@@ -68,21 +134,49 @@ B905.py:9:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
10 |
|
||||
11 | # OK
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:24:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
6 6 | zip("a", "b")
|
||||
7 7 | zip("a", "b", *zip("c"))
|
||||
8 8 | zip(zip("a"), strict=False)
|
||||
9 |-zip(zip("a", strict=True))
|
||||
9 |+zip(zip("a", strict=True), strict=False)
|
||||
10 10 |
|
||||
11 11 | # OK
|
||||
12 12 | zip(range(3), strict=True)
|
||||
|
||||
B905.py:24:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
23 | # Errors (limited iterators).
|
||||
24 | zip([1, 2, 3], repeat(1, 1))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905
|
||||
25 | zip([1, 2, 3], repeat(1, times=4))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
B905.py:25:1: B905 `zip()` without an explicit `strict=` parameter
|
||||
ℹ Safe fix
|
||||
21 21 | zip([1, 2, 3], repeat(1, times=None))
|
||||
22 22 |
|
||||
23 23 | # Errors (limited iterators).
|
||||
24 |-zip([1, 2, 3], repeat(1, 1))
|
||||
24 |+zip([1, 2, 3], repeat(1, 1), strict=False)
|
||||
25 25 | zip([1, 2, 3], repeat(1, times=4))
|
||||
|
||||
B905.py:25:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
23 | # Errors (limited iterators).
|
||||
24 | zip([1, 2, 3], repeat(1, 1))
|
||||
25 | zip([1, 2, 3], repeat(1, times=4))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
ℹ Safe fix
|
||||
22 22 |
|
||||
23 23 | # Errors (limited iterators).
|
||||
24 24 | zip([1, 2, 3], repeat(1, 1))
|
||||
25 |-zip([1, 2, 3], repeat(1, times=4))
|
||||
25 |+zip([1, 2, 3], repeat(1, times=4), strict=False)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -48,21 +47,12 @@ impl Violation for DjangoAllWithModelForm {
|
||||
}
|
||||
|
||||
/// DJ007
|
||||
pub(crate) fn all_with_model_form(
|
||||
checker: &Checker,
|
||||
arguments: Option<&Arguments>,
|
||||
body: &[Stmt],
|
||||
) -> Option<Diagnostic> {
|
||||
if !arguments.is_some_and(|arguments| {
|
||||
arguments
|
||||
.args
|
||||
.iter()
|
||||
.any(|base| is_model_form(base, checker.semantic()))
|
||||
}) {
|
||||
return None;
|
||||
pub(crate) fn all_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
if !is_model_form(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for element in body {
|
||||
for element in &class_def.body {
|
||||
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
|
||||
continue;
|
||||
};
|
||||
@@ -83,12 +73,18 @@ pub(crate) fn all_with_model_form(
|
||||
match value.as_ref() {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
|
||||
if value == "__all__" {
|
||||
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DjangoAllWithModelForm, element.range()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => {
|
||||
if value == "__all__".as_bytes() {
|
||||
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DjangoAllWithModelForm, element.range()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
@@ -96,5 +92,4 @@ pub(crate) fn all_with_model_form(
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -46,21 +45,12 @@ impl Violation for DjangoExcludeWithModelForm {
|
||||
}
|
||||
|
||||
/// DJ006
|
||||
pub(crate) fn exclude_with_model_form(
|
||||
checker: &Checker,
|
||||
arguments: Option<&Arguments>,
|
||||
body: &[Stmt],
|
||||
) -> Option<Diagnostic> {
|
||||
if !arguments.is_some_and(|arguments| {
|
||||
arguments
|
||||
.args
|
||||
.iter()
|
||||
.any(|base| is_model_form(base, checker.semantic()))
|
||||
}) {
|
||||
return None;
|
||||
pub(crate) fn exclude_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
if !is_model_form(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for element in body {
|
||||
for element in &class_def.body {
|
||||
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
|
||||
continue;
|
||||
};
|
||||
@@ -76,10 +66,12 @@ pub(crate) fn exclude_with_model_form(
|
||||
continue;
|
||||
};
|
||||
if id == "exclude" {
|
||||
return Some(Diagnostic::new(DjangoExcludeWithModelForm, target.range()));
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DjangoExcludeWithModelForm, target.range()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::{analyze, SemanticModel};
|
||||
|
||||
/// Return `true` if a Python class appears to be a Django model, based on its base classes.
|
||||
pub(super) fn is_model(base: &Expr, semantic: &SemanticModel) -> bool {
|
||||
semantic.resolve_call_path(base).is_some_and(|call_path| {
|
||||
pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_over_body(class_def, semantic, &|call_path| {
|
||||
matches!(call_path.as_slice(), ["django", "db", "models", "Model"])
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if a Python class appears to be a Django model form, based on its base classes.
|
||||
pub(super) fn is_model_form(base: &Expr, semantic: &SemanticModel) -> bool {
|
||||
semantic.resolve_call_path(base).is_some_and(|call_path| {
|
||||
pub(super) fn is_model_form(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_over_body(class_def, semantic, &|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_const_true;
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -52,57 +51,39 @@ impl Violation for DjangoModelWithoutDunderStr {
|
||||
}
|
||||
|
||||
/// DJ008
|
||||
pub(crate) fn model_without_dunder_str(
|
||||
checker: &mut Checker,
|
||||
ast::StmtClassDef {
|
||||
name,
|
||||
arguments,
|
||||
body,
|
||||
..
|
||||
}: &ast::StmtClassDef,
|
||||
) {
|
||||
if !is_non_abstract_model(arguments.as_deref(), body, checker.semantic()) {
|
||||
pub(crate) fn model_without_dunder_str(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
if !is_non_abstract_model(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
if has_dunder_method(body) {
|
||||
if has_dunder_method(class_def) {
|
||||
return;
|
||||
}
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DjangoModelWithoutDunderStr, name.range()));
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
DjangoModelWithoutDunderStr,
|
||||
class_def.identifier(),
|
||||
));
|
||||
}
|
||||
|
||||
fn has_dunder_method(body: &[Stmt]) -> bool {
|
||||
body.iter().any(|val| match val {
|
||||
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => {
|
||||
if name == "__str__" {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
/// Returns `true` if the class has `__str__` method.
|
||||
fn has_dunder_method(class_def: &ast::StmtClassDef) -> bool {
|
||||
class_def.body.iter().any(|val| match val {
|
||||
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => name == "__str__",
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_non_abstract_model(
|
||||
arguments: Option<&Arguments>,
|
||||
body: &[Stmt],
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
let Some(Arguments { args: bases, .. }) = arguments else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if is_model_abstract(body) {
|
||||
return false;
|
||||
/// Returns `true` if the class is a non-abstract Django model.
|
||||
fn is_non_abstract_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
if class_def.bases().is_empty() || is_model_abstract(class_def) {
|
||||
false
|
||||
} else {
|
||||
helpers::is_model(class_def, semantic)
|
||||
}
|
||||
|
||||
bases.iter().any(|base| helpers::is_model(base, semantic))
|
||||
}
|
||||
|
||||
/// Check if class is abstract, in terms of Django model inheritance.
|
||||
fn is_model_abstract(body: &[Stmt]) -> bool {
|
||||
for element in body {
|
||||
fn is_model_abstract(class_def: &ast::StmtClassDef) -> bool {
|
||||
for element in &class_def.body {
|
||||
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -79,6 +78,50 @@ impl Violation for DjangoUnorderedBodyContentInModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// DJ012
|
||||
pub(crate) fn unordered_body_content_in_model(
|
||||
checker: &mut Checker,
|
||||
class_def: &ast::StmtClassDef,
|
||||
) {
|
||||
if !helpers::is_model(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Track all the element types we've seen so far.
|
||||
let mut element_types = Vec::new();
|
||||
let mut prev_element_type = None;
|
||||
for element in &class_def.body {
|
||||
let Some(element_type) = get_element_type(element, checker.semantic()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Skip consecutive elements of the same type. It's less noisy to only report
|
||||
// violations at type boundaries (e.g., avoid raising a violation for _every_
|
||||
// field declaration that's out of order).
|
||||
if prev_element_type == Some(element_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
prev_element_type = Some(element_type);
|
||||
|
||||
if let Some(&prev_element_type) = element_types
|
||||
.iter()
|
||||
.find(|&&prev_element_type| prev_element_type > element_type)
|
||||
{
|
||||
let diagnostic = Diagnostic::new(
|
||||
DjangoUnorderedBodyContentInModel {
|
||||
element_type,
|
||||
prev_element_type,
|
||||
},
|
||||
element.range(),
|
||||
);
|
||||
checker.diagnostics.push(diagnostic);
|
||||
} else {
|
||||
element_types.push(element_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
|
||||
enum ContentType {
|
||||
FieldDeclaration,
|
||||
@@ -140,53 +183,3 @@ fn get_element_type(element: &Stmt, semantic: &SemanticModel) -> Option<ContentT
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// DJ012
|
||||
pub(crate) fn unordered_body_content_in_model(
|
||||
checker: &mut Checker,
|
||||
arguments: Option<&Arguments>,
|
||||
body: &[Stmt],
|
||||
) {
|
||||
if !arguments.is_some_and(|arguments| {
|
||||
arguments
|
||||
.args
|
||||
.iter()
|
||||
.any(|base| helpers::is_model(base, checker.semantic()))
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Track all the element types we've seen so far.
|
||||
let mut element_types = Vec::new();
|
||||
let mut prev_element_type = None;
|
||||
for element in body {
|
||||
let Some(element_type) = get_element_type(element, checker.semantic()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Skip consecutive elements of the same type. It's less noisy to only report
|
||||
// violations at type boundaries (e.g., avoid raising a violation for _every_
|
||||
// field declaration that's out of order).
|
||||
if prev_element_type == Some(element_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
prev_element_type = Some(element_type);
|
||||
|
||||
if let Some(&prev_element_type) = element_types
|
||||
.iter()
|
||||
.find(|&&prev_element_type| prev_element_type > element_type)
|
||||
{
|
||||
let diagnostic = Diagnostic::new(
|
||||
DjangoUnorderedBodyContentInModel {
|
||||
element_type,
|
||||
prev_element_type,
|
||||
},
|
||||
element.range(),
|
||||
);
|
||||
checker.diagnostics.push(diagnostic);
|
||||
} else {
|
||||
element_types.push(element_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,4 +54,12 @@ DJ012.py:129:5: DJ012 Order of model's inner classes, methods, and fields does n
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012
|
||||
|
|
||||
|
||||
DJ012.py:146:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class
|
||||
|
|
||||
144 | return "foobar"
|
||||
145 |
|
||||
146 | first_name = models.CharField(max_length=32)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::delete_stmt;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
@@ -28,14 +28,24 @@ use crate::registry::AsRule;
|
||||
/// def add_numbers(a, b):
|
||||
/// return a + b
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as it may remove `print` statements
|
||||
/// that are used beyond debugging purposes.
|
||||
#[violation]
|
||||
pub struct Print;
|
||||
|
||||
impl Violation for Print {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`print` found")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Remove `print`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
@@ -65,19 +75,29 @@ impl Violation for Print {
|
||||
/// dict_c = {**dict_a, **dict_b}
|
||||
/// return dict_c
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as it may remove `pprint` statements
|
||||
/// that are used beyond debugging purposes.
|
||||
#[violation]
|
||||
pub struct PPrint;
|
||||
|
||||
impl Violation for PPrint {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`pprint` found")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Remove `pprint`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// T201, T203
|
||||
pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let diagnostic = {
|
||||
let mut diagnostic = {
|
||||
let call_path = checker.semantic().resolve_call_path(&call.func);
|
||||
if call_path
|
||||
.as_ref()
|
||||
@@ -113,5 +133,15 @@ pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the `print`, if it's a standalone statement.
|
||||
if checker.semantic().current_expression_parent().is_none() {
|
||||
let statement = checker.semantic().current_statement();
|
||||
let parent = checker.semantic().current_statement_parent();
|
||||
let edit = delete_stmt(statement, parent, checker.locator(), checker.indexer());
|
||||
diagnostic.set_fix(Fix::unsafe_edit(edit).isolate(Checker::isolation(
|
||||
checker.semantic().current_statement_parent_id(),
|
||||
)));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_print/mod.rs
|
||||
---
|
||||
T201.py:4:1: T201 `print` found
|
||||
T201.py:4:1: T201 [*] `print` found
|
||||
|
|
||||
2 | import tempfile
|
||||
3 |
|
||||
@@ -10,8 +10,18 @@ T201.py:4:1: T201 `print` found
|
||||
5 | print("Hello, world!", file=None) # T201
|
||||
6 | print("Hello, world!", file=sys.stdout) # T201
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
T201.py:5:1: T201 `print` found
|
||||
ℹ Unsafe fix
|
||||
1 1 | import sys
|
||||
2 2 | import tempfile
|
||||
3 3 |
|
||||
4 |-print("Hello, world!") # T201
|
||||
5 4 | print("Hello, world!", file=None) # T201
|
||||
6 5 | print("Hello, world!", file=sys.stdout) # T201
|
||||
7 6 | print("Hello, world!", file=sys.stderr) # T201
|
||||
|
||||
T201.py:5:1: T201 [*] `print` found
|
||||
|
|
||||
4 | print("Hello, world!") # T201
|
||||
5 | print("Hello, world!", file=None) # T201
|
||||
@@ -19,8 +29,18 @@ T201.py:5:1: T201 `print` found
|
||||
6 | print("Hello, world!", file=sys.stdout) # T201
|
||||
7 | print("Hello, world!", file=sys.stderr) # T201
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
T201.py:6:1: T201 `print` found
|
||||
ℹ Unsafe fix
|
||||
2 2 | import tempfile
|
||||
3 3 |
|
||||
4 4 | print("Hello, world!") # T201
|
||||
5 |-print("Hello, world!", file=None) # T201
|
||||
6 5 | print("Hello, world!", file=sys.stdout) # T201
|
||||
7 6 | print("Hello, world!", file=sys.stderr) # T201
|
||||
8 7 |
|
||||
|
||||
T201.py:6:1: T201 [*] `print` found
|
||||
|
|
||||
4 | print("Hello, world!") # T201
|
||||
5 | print("Hello, world!", file=None) # T201
|
||||
@@ -28,8 +48,18 @@ T201.py:6:1: T201 `print` found
|
||||
| ^^^^^ T201
|
||||
7 | print("Hello, world!", file=sys.stderr) # T201
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
T201.py:7:1: T201 `print` found
|
||||
ℹ Unsafe fix
|
||||
3 3 |
|
||||
4 4 | print("Hello, world!") # T201
|
||||
5 5 | print("Hello, world!", file=None) # T201
|
||||
6 |-print("Hello, world!", file=sys.stdout) # T201
|
||||
7 6 | print("Hello, world!", file=sys.stderr) # T201
|
||||
8 7 |
|
||||
9 8 | with tempfile.NamedTemporaryFile() as fp:
|
||||
|
||||
T201.py:7:1: T201 [*] `print` found
|
||||
|
|
||||
5 | print("Hello, world!", file=None) # T201
|
||||
6 | print("Hello, world!", file=sys.stdout) # T201
|
||||
@@ -38,5 +68,15 @@ T201.py:7:1: T201 `print` found
|
||||
8 |
|
||||
9 | with tempfile.NamedTemporaryFile() as fp:
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
4 4 | print("Hello, world!") # T201
|
||||
5 5 | print("Hello, world!", file=None) # T201
|
||||
6 6 | print("Hello, world!", file=sys.stdout) # T201
|
||||
7 |-print("Hello, world!", file=sys.stderr) # T201
|
||||
8 7 |
|
||||
9 8 | with tempfile.NamedTemporaryFile() as fp:
|
||||
10 9 | print("Hello, world!", file=fp) # OK
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_print/mod.rs
|
||||
---
|
||||
T203.py:3:1: T203 `pprint` found
|
||||
T203.py:3:1: T203 [*] `pprint` found
|
||||
|
|
||||
1 | from pprint import pprint
|
||||
2 |
|
||||
@@ -10,8 +10,17 @@ T203.py:3:1: T203 `pprint` found
|
||||
4 |
|
||||
5 | import pprint
|
||||
|
|
||||
= help: Remove `pprint`
|
||||
|
||||
T203.py:7:1: T203 `pprint` found
|
||||
ℹ Unsafe fix
|
||||
1 1 | from pprint import pprint
|
||||
2 2 |
|
||||
3 |-pprint("Hello, world!") # T203
|
||||
4 3 |
|
||||
5 4 | import pprint
|
||||
6 5 |
|
||||
|
||||
T203.py:7:1: T203 [*] `pprint` found
|
||||
|
|
||||
5 | import pprint
|
||||
6 |
|
||||
@@ -20,5 +29,14 @@ T203.py:7:1: T203 `pprint` found
|
||||
8 |
|
||||
9 | pprint.pformat("Hello, world!")
|
||||
|
|
||||
= help: Remove `pprint`
|
||||
|
||||
ℹ Unsafe fix
|
||||
4 4 |
|
||||
5 5 | import pprint
|
||||
6 6 |
|
||||
7 |-pprint.pprint("Hello, world!") # T203
|
||||
8 7 |
|
||||
9 8 | pprint.pformat("Hello, world!")
|
||||
|
||||
|
||||
|
||||
@@ -80,17 +80,24 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
|
||||
}
|
||||
|
||||
let mut type_exprs = Vec::new();
|
||||
let mut other_exprs = Vec::new();
|
||||
|
||||
let mut collect_type_exprs = |expr: &'a Expr, _| {
|
||||
let Some(subscript) = expr.as_subscript_expr() else {
|
||||
return;
|
||||
};
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(subscript.value.as_ref())
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["" | "builtins", "type"]))
|
||||
{
|
||||
type_exprs.push(&subscript.slice);
|
||||
let subscript = expr.as_subscript_expr();
|
||||
|
||||
if subscript.is_none() {
|
||||
other_exprs.push(expr);
|
||||
} else {
|
||||
let unwrapped = subscript.unwrap();
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(unwrapped.value.as_ref())
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["" | "builtins", "type"]))
|
||||
{
|
||||
type_exprs.push(&unwrapped.slice);
|
||||
} else {
|
||||
other_exprs.push(expr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,55 +120,82 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
|
||||
|
||||
if checker.semantic().is_builtin("type") {
|
||||
let content = if let Some(subscript) = subscript {
|
||||
checker
|
||||
.generator()
|
||||
.expr(&Expr::Subscript(ast::ExprSubscript {
|
||||
value: Box::new(Expr::Name(ast::ExprName {
|
||||
id: "type".into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
slice: Box::new(Expr::Subscript(ast::ExprSubscript {
|
||||
value: subscript.value.clone(),
|
||||
slice: Box::new(Expr::Tuple(ast::ExprTuple {
|
||||
elts: type_members
|
||||
.into_iter()
|
||||
.map(|type_member| {
|
||||
Expr::Name(ast::ExprName {
|
||||
id: type_member,
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
let types = &Expr::Subscript(ast::ExprSubscript {
|
||||
value: Box::new(Expr::Name(ast::ExprName {
|
||||
id: "type".into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
}))
|
||||
} else {
|
||||
checker
|
||||
.generator()
|
||||
.expr(&Expr::Subscript(ast::ExprSubscript {
|
||||
value: Box::new(Expr::Name(ast::ExprName {
|
||||
id: "type".into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
slice: Box::new(concatenate_bin_ors(
|
||||
type_exprs
|
||||
.clone()
|
||||
})),
|
||||
slice: Box::new(Expr::Subscript(ast::ExprSubscript {
|
||||
value: subscript.value.clone(),
|
||||
slice: Box::new(Expr::Tuple(ast::ExprTuple {
|
||||
elts: type_members
|
||||
.into_iter()
|
||||
.map(std::convert::AsRef::as_ref)
|
||||
.map(|type_member| {
|
||||
Expr::Name(ast::ExprName {
|
||||
id: type_member,
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
}))
|
||||
})),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
if other_exprs.is_empty() {
|
||||
checker.generator().expr(types)
|
||||
} else {
|
||||
let mut exprs = Vec::new();
|
||||
exprs.push(types);
|
||||
exprs.extend(other_exprs);
|
||||
|
||||
let union = Expr::Subscript(ast::ExprSubscript {
|
||||
value: subscript.value.clone(),
|
||||
slice: Box::new(Expr::Tuple(ast::ExprTuple {
|
||||
elts: exprs.into_iter().cloned().collect(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
checker.generator().expr(&union)
|
||||
}
|
||||
} else {
|
||||
let types = &Expr::Subscript(ast::ExprSubscript {
|
||||
value: Box::new(Expr::Name(ast::ExprName {
|
||||
id: "type".into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
slice: Box::new(concatenate_bin_ors(
|
||||
type_exprs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(std::convert::AsRef::as_ref)
|
||||
.collect(),
|
||||
)),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
if other_exprs.is_empty() {
|
||||
checker.generator().expr(types)
|
||||
} else {
|
||||
let mut exprs = Vec::new();
|
||||
exprs.push(types);
|
||||
exprs.extend(other_exprs);
|
||||
|
||||
checker.generator().expr(&concatenate_bin_ors(exprs))
|
||||
}
|
||||
};
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
|
||||
@@ -7,28 +7,35 @@ use ruff_text_size::Ranged;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the presence of unused private `TypeVar` declarations.
|
||||
/// Checks for the presence of unused private `TypeVar`, `ParamSpec` or
|
||||
/// `TypeVarTuple` declarations.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// A private `TypeVar` that is defined but not used is likely a mistake, and
|
||||
/// A private `TypeVar` that is defined but not used is likely a mistake. It
|
||||
/// should either be used, made public, or removed to avoid confusion.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import typing
|
||||
/// import typing_extensions
|
||||
///
|
||||
/// _T = typing.TypeVar("_T")
|
||||
/// _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct UnusedPrivateTypeVar {
|
||||
name: String,
|
||||
type_var_like_name: String,
|
||||
type_var_like_kind: String,
|
||||
}
|
||||
|
||||
impl Violation for UnusedPrivateTypeVar {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let UnusedPrivateTypeVar { name } = self;
|
||||
format!("Private TypeVar `{name}` is never used")
|
||||
let UnusedPrivateTypeVar {
|
||||
type_var_like_name,
|
||||
type_var_like_kind,
|
||||
} = self;
|
||||
format!("Private {type_var_like_kind} `{type_var_like_name}` is never used")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,13 +192,26 @@ pub(crate) fn unused_private_type_var(
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
if !checker.semantic().match_typing_expr(func, "TypeVar") {
|
||||
|
||||
let semantic = checker.semantic();
|
||||
let Some(type_var_like_kind) = semantic.resolve_call_path(func).and_then(|call_path| {
|
||||
if semantic.match_typing_call_path(&call_path, "TypeVar") {
|
||||
Some("TypeVar")
|
||||
} else if semantic.match_typing_call_path(&call_path, "ParamSpec") {
|
||||
Some("ParamSpec")
|
||||
} else if semantic.match_typing_call_path(&call_path, "TypeVarTuple") {
|
||||
Some("TypeVarTuple")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
diagnostics.push(Diagnostic::new(
|
||||
UnusedPrivateTypeVar {
|
||||
name: id.to_string(),
|
||||
type_var_like_name: id.to_string(),
|
||||
type_var_like_kind: type_var_like_kind.to_string(),
|
||||
},
|
||||
binding.range(),
|
||||
));
|
||||
|
||||
@@ -1,22 +1,52 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI018.py:4:1: PYI018 Private TypeVar `_T` is never used
|
||||
PYI018.py:6:1: PYI018 Private TypeVar `_T` is never used
|
||||
|
|
||||
2 | from typing import TypeVar
|
||||
3 |
|
||||
4 | _T = typing.TypeVar("_T")
|
||||
4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 |
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
| ^^ PYI018
|
||||
5 | _P = TypeVar("_P")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
|
|
||||
|
||||
PYI018.py:5:1: PYI018 Private TypeVar `_P` is never used
|
||||
PYI018.py:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
||||
|
|
||||
4 | _T = typing.TypeVar("_T")
|
||||
5 | _P = TypeVar("_P")
|
||||
| ^^ PYI018
|
||||
6 |
|
||||
7 | # OK
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
| ^^^ PYI018
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
|
|
||||
|
||||
PYI018.py:8:1: PYI018 Private ParamSpec `_P` is never used
|
||||
|
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
| ^^ PYI018
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
|
||||
PYI018.py:9:1: PYI018 Private ParamSpec `_P2` is never used
|
||||
|
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
| ^^^ PYI018
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
|
||||
PYI018.py:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
||||
|
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
| ^^^^ PYI018
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +1,52 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI018.pyi:4:1: PYI018 Private TypeVar `_T` is never used
|
||||
PYI018.pyi:6:1: PYI018 Private TypeVar `_T` is never used
|
||||
|
|
||||
2 | from typing import TypeVar
|
||||
3 |
|
||||
4 | _T = typing.TypeVar("_T")
|
||||
4 | from typing_extensions import ParamSpec, TypeVarTuple
|
||||
5 |
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
| ^^ PYI018
|
||||
5 | _P = TypeVar("_P")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
|
|
||||
|
||||
PYI018.pyi:5:1: PYI018 Private TypeVar `_P` is never used
|
||||
PYI018.pyi:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
||||
|
|
||||
4 | _T = typing.TypeVar("_T")
|
||||
5 | _P = TypeVar("_P")
|
||||
| ^^ PYI018
|
||||
6 |
|
||||
7 | # OK
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
| ^^^ PYI018
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
|
|
||||
|
||||
PYI018.pyi:8:1: PYI018 Private ParamSpec `_P` is never used
|
||||
|
|
||||
6 | _T = typing.TypeVar("_T")
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
| ^^ PYI018
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
|
||||
PYI018.pyi:9:1: PYI018 Private ParamSpec `_P2` is never used
|
||||
|
|
||||
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
| ^^^ PYI018
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
|
|
||||
|
||||
PYI018.pyi:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
||||
|
|
||||
8 | _P = ParamSpec("_P")
|
||||
9 | _P2 = typing.ParamSpec("_P2")
|
||||
10 | _Ts2 = TypeVarTuple("_Ts2")
|
||||
| ^^^^ PYI018
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -54,5 +54,91 @@ PYI055.py:39:8: PYI055 [*] Multiple `type` members in a union. Combine them into
|
||||
38 38 | # PYI055
|
||||
39 |- x: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker
|
||||
39 |+ x: type[Union[requests_mock.Mocker, httpretty, str]] = requests_mock.Mocker
|
||||
40 40 |
|
||||
41 41 |
|
||||
42 42 | def convert_union(union: UnionType) -> _T | None:
|
||||
|
||||
PYI055.py:44:9: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`.
|
||||
|
|
||||
42 | def convert_union(union: UnionType) -> _T | None:
|
||||
43 | converters: tuple[
|
||||
44 | type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
45 | ] = union.__args__
|
||||
46 | ...
|
||||
|
|
||||
= help: Combine multiple `type` members
|
||||
|
||||
ℹ Safe fix
|
||||
41 41 |
|
||||
42 42 | def convert_union(union: UnionType) -> _T | None:
|
||||
43 43 | converters: tuple[
|
||||
44 |- type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
44 |+ type[_T | Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
45 45 | ] = union.__args__
|
||||
46 46 | ...
|
||||
47 47 |
|
||||
|
||||
PYI055.py:50:15: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`.
|
||||
|
|
||||
48 | def convert_union(union: UnionType) -> _T | None:
|
||||
49 | converters: tuple[
|
||||
50 | Union[type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
51 | ] = union.__args__
|
||||
52 | ...
|
||||
|
|
||||
= help: Combine multiple `type` members
|
||||
|
||||
ℹ Safe fix
|
||||
47 47 |
|
||||
48 48 | def convert_union(union: UnionType) -> _T | None:
|
||||
49 49 | converters: tuple[
|
||||
50 |- Union[type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055
|
||||
50 |+ Union[type[_T | Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055
|
||||
51 51 | ] = union.__args__
|
||||
52 52 | ...
|
||||
53 53 |
|
||||
|
||||
PYI055.py:56:15: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`.
|
||||
|
|
||||
54 | def convert_union(union: UnionType) -> _T | None:
|
||||
55 | converters: tuple[
|
||||
56 | Union[type[_T] | type[Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
57 | ] = union.__args__
|
||||
58 | ...
|
||||
|
|
||||
= help: Combine multiple `type` members
|
||||
|
||||
ℹ Safe fix
|
||||
53 53 |
|
||||
54 54 | def convert_union(union: UnionType) -> _T | None:
|
||||
55 55 | converters: tuple[
|
||||
56 |- Union[type[_T] | type[Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
56 |+ Union[type[_T | Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
57 57 | ] = union.__args__
|
||||
58 58 | ...
|
||||
59 59 |
|
||||
|
||||
PYI055.py:62:15: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`.
|
||||
|
|
||||
60 | def convert_union(union: UnionType) -> _T | None:
|
||||
61 | converters: tuple[
|
||||
62 | Union[type[_T] | type[Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
63 | ] = union.__args__
|
||||
64 | ...
|
||||
|
|
||||
= help: Combine multiple `type` members
|
||||
|
||||
ℹ Safe fix
|
||||
59 59 |
|
||||
60 60 | def convert_union(union: UnionType) -> _T | None:
|
||||
61 61 | converters: tuple[
|
||||
62 |- Union[type[_T] | type[Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
62 |+ Union[type[_T | Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055
|
||||
63 63 | ] = union.__args__
|
||||
64 64 | ...
|
||||
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ PYI055.pyi:10:15: PYI055 [*] Multiple `type` members in a union. Combine them in
|
||||
8 8 | z: Union[type[float, int], type[complex]]
|
||||
9 9 |
|
||||
10 |-def func(arg: type[int] | str | type[float]) -> None: ...
|
||||
10 |+def func(arg: type[int | float]) -> None: ...
|
||||
10 |+def func(arg: type[int | float] | str) -> None: ...
|
||||
11 11 |
|
||||
12 12 | # OK
|
||||
13 13 | x: type[int, str, float]
|
||||
|
||||
@@ -56,6 +56,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Rule::InDictKeys, Path::new("SIM118.py"))]
|
||||
#[test_case(Rule::YodaConditions, Path::new("SIM300.py"))]
|
||||
#[test_case(Rule::IfElseBlockInsteadOfDictGet, Path::new("SIM401.py"))]
|
||||
#[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::cmp;
|
||||
|
||||
use anyhow::Result;
|
||||
use libcst_native::CompOp;
|
||||
|
||||
@@ -14,6 +16,7 @@ use crate::cst::helpers::or_space;
|
||||
use crate::cst::matchers::{match_comparison, transform_expression};
|
||||
use crate::fix::edits::pad;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
use crate::settings::types::PreviewMode;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for conditions that position a constant on the left-hand side of the
|
||||
@@ -78,18 +81,65 @@ impl Violation for YodaConditions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if an [`Expr`] is a constant or a constant-like name.
|
||||
fn is_constant_like(expr: &Expr) -> bool {
|
||||
match expr {
|
||||
Expr::Attribute(ast::ExprAttribute { attr, .. }) => str::is_cased_uppercase(attr),
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(is_constant_like),
|
||||
Expr::Name(ast::ExprName { id, .. }) => str::is_cased_uppercase(id),
|
||||
Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert,
|
||||
operand,
|
||||
range: _,
|
||||
}) => operand.is_literal_expr(),
|
||||
_ => expr.is_literal_expr(),
|
||||
/// Comparisons left-hand side must not be more [`ConstantLikelihood`] than the right-hand side.
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
enum ConstantLikelihood {
|
||||
/// The expression is unlikely to be a constant (e.g., `foo` or `foo(bar)`).
|
||||
Unlikely = 0,
|
||||
|
||||
/// The expression is likely to be a constant (e.g., `FOO`).
|
||||
Probably = 1,
|
||||
|
||||
/// The expression is definitely a constant (e.g., `42` or `"foo"`).
|
||||
Definitely = 2,
|
||||
}
|
||||
|
||||
impl ConstantLikelihood {
|
||||
/// Determine the [`ConstantLikelihood`] of an expression.
|
||||
fn from_expression(expr: &Expr, preview: PreviewMode) -> Self {
|
||||
match expr {
|
||||
_ if expr.is_literal_expr() => ConstantLikelihood::Definitely,
|
||||
Expr::Attribute(ast::ExprAttribute { attr, .. }) => {
|
||||
ConstantLikelihood::from_identifier(attr)
|
||||
}
|
||||
Expr::Name(ast::ExprName { id, .. }) => ConstantLikelihood::from_identifier(id),
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
|
||||
.iter()
|
||||
.map(|expr| ConstantLikelihood::from_expression(expr, preview))
|
||||
.min()
|
||||
.unwrap_or(ConstantLikelihood::Definitely),
|
||||
Expr::List(ast::ExprList { elts, .. }) if preview.is_enabled() => elts
|
||||
.iter()
|
||||
.map(|expr| ConstantLikelihood::from_expression(expr, preview))
|
||||
.min()
|
||||
.unwrap_or(ConstantLikelihood::Definitely),
|
||||
Expr::Dict(ast::ExprDict { values: vs, .. }) if preview.is_enabled() => {
|
||||
if vs.is_empty() {
|
||||
ConstantLikelihood::Definitely
|
||||
} else {
|
||||
ConstantLikelihood::Probably
|
||||
}
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp { left, right, .. }) => cmp::min(
|
||||
ConstantLikelihood::from_expression(left, preview),
|
||||
ConstantLikelihood::from_expression(right, preview),
|
||||
),
|
||||
Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert,
|
||||
operand,
|
||||
range: _,
|
||||
}) => ConstantLikelihood::from_expression(operand, preview),
|
||||
_ => ConstantLikelihood::Unlikely,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine the [`ConstantLikelihood`] of an identifier.
|
||||
fn from_identifier(identifier: &str) -> Self {
|
||||
if str::is_cased_uppercase(identifier) {
|
||||
ConstantLikelihood::Probably
|
||||
} else {
|
||||
ConstantLikelihood::Unlikely
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +230,9 @@ pub(crate) fn yoda_conditions(
|
||||
return;
|
||||
}
|
||||
|
||||
if !is_constant_like(left) || is_constant_like(right) {
|
||||
if ConstantLikelihood::from_expression(left, checker.settings.preview)
|
||||
<= ConstantLikelihood::from_expression(right, checker.settings.preview)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ SIM300.py:2:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda
|
||||
1 | # Errors
|
||||
2 | "yoda" == compare # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^ SIM300
|
||||
3 | "yoda" == compare # SIM300
|
||||
4 | 42 == age # SIM300
|
||||
3 | 42 == age # SIM300
|
||||
4 | ("a", "b") == compare # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `compare == "yoda"`
|
||||
|
||||
@@ -15,342 +15,340 @@ SIM300.py:2:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda
|
||||
1 1 | # Errors
|
||||
2 |-"yoda" == compare # SIM300
|
||||
2 |+compare == "yoda" # SIM300
|
||||
3 3 | "yoda" == compare # SIM300
|
||||
4 4 | 42 == age # SIM300
|
||||
5 5 | ("a", "b") == compare # SIM300
|
||||
3 3 | 42 == age # SIM300
|
||||
4 4 | ("a", "b") == compare # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
|
||||
SIM300.py:3:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda"` instead
|
||||
SIM300.py:3:1: SIM300 [*] Yoda conditions are discouraged, use `age == 42` instead
|
||||
|
|
||||
1 | # Errors
|
||||
2 | "yoda" == compare # SIM300
|
||||
3 | "yoda" == compare # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^ SIM300
|
||||
4 | 42 == age # SIM300
|
||||
5 | ("a", "b") == compare # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `compare == "yoda"`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 2 | "yoda" == compare # SIM300
|
||||
3 |-"yoda" == compare # SIM300
|
||||
3 |+compare == "yoda" # SIM300
|
||||
4 4 | 42 == age # SIM300
|
||||
5 5 | ("a", "b") == compare # SIM300
|
||||
6 6 | "yoda" <= compare # SIM300
|
||||
|
||||
SIM300.py:4:1: SIM300 [*] Yoda conditions are discouraged, use `age == 42` instead
|
||||
|
|
||||
2 | "yoda" == compare # SIM300
|
||||
3 | "yoda" == compare # SIM300
|
||||
4 | 42 == age # SIM300
|
||||
3 | 42 == age # SIM300
|
||||
| ^^^^^^^^^ SIM300
|
||||
5 | ("a", "b") == compare # SIM300
|
||||
6 | "yoda" <= compare # SIM300
|
||||
4 | ("a", "b") == compare # SIM300
|
||||
5 | "yoda" <= compare # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age == 42`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 2 | "yoda" == compare # SIM300
|
||||
3 3 | "yoda" == compare # SIM300
|
||||
4 |-42 == age # SIM300
|
||||
4 |+age == 42 # SIM300
|
||||
5 5 | ("a", "b") == compare # SIM300
|
||||
6 6 | "yoda" <= compare # SIM300
|
||||
7 7 | "yoda" < compare # SIM300
|
||||
3 |-42 == age # SIM300
|
||||
3 |+age == 42 # SIM300
|
||||
4 4 | ("a", "b") == compare # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
|
||||
SIM300.py:5:1: SIM300 [*] Yoda conditions are discouraged, use `compare == ("a", "b")` instead
|
||||
SIM300.py:4:1: SIM300 [*] Yoda conditions are discouraged, use `compare == ("a", "b")` instead
|
||||
|
|
||||
3 | "yoda" == compare # SIM300
|
||||
4 | 42 == age # SIM300
|
||||
5 | ("a", "b") == compare # SIM300
|
||||
2 | "yoda" == compare # SIM300
|
||||
3 | 42 == age # SIM300
|
||||
4 | ("a", "b") == compare # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ SIM300
|
||||
6 | "yoda" <= compare # SIM300
|
||||
7 | "yoda" < compare # SIM300
|
||||
5 | "yoda" <= compare # SIM300
|
||||
6 | "yoda" < compare # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `compare == ("a", "b")`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 2 | "yoda" == compare # SIM300
|
||||
3 3 | "yoda" == compare # SIM300
|
||||
4 4 | 42 == age # SIM300
|
||||
5 |-("a", "b") == compare # SIM300
|
||||
5 |+compare == ("a", "b") # SIM300
|
||||
6 6 | "yoda" <= compare # SIM300
|
||||
7 7 | "yoda" < compare # SIM300
|
||||
8 8 | 42 > age # SIM300
|
||||
3 3 | 42 == age # SIM300
|
||||
4 |-("a", "b") == compare # SIM300
|
||||
4 |+compare == ("a", "b") # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
|
||||
SIM300.py:6:1: SIM300 [*] Yoda conditions are discouraged, use `compare >= "yoda"` instead
|
||||
SIM300.py:5:1: SIM300 [*] Yoda conditions are discouraged, use `compare >= "yoda"` instead
|
||||
|
|
||||
4 | 42 == age # SIM300
|
||||
5 | ("a", "b") == compare # SIM300
|
||||
6 | "yoda" <= compare # SIM300
|
||||
3 | 42 == age # SIM300
|
||||
4 | ("a", "b") == compare # SIM300
|
||||
5 | "yoda" <= compare # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^ SIM300
|
||||
7 | "yoda" < compare # SIM300
|
||||
8 | 42 > age # SIM300
|
||||
6 | "yoda" < compare # SIM300
|
||||
7 | 42 > age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `compare >= "yoda"`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | "yoda" == compare # SIM300
|
||||
4 4 | 42 == age # SIM300
|
||||
5 5 | ("a", "b") == compare # SIM300
|
||||
6 |-"yoda" <= compare # SIM300
|
||||
6 |+compare >= "yoda" # SIM300
|
||||
7 7 | "yoda" < compare # SIM300
|
||||
8 8 | 42 > age # SIM300
|
||||
9 9 | -42 > age # SIM300
|
||||
2 2 | "yoda" == compare # SIM300
|
||||
3 3 | 42 == age # SIM300
|
||||
4 4 | ("a", "b") == compare # SIM300
|
||||
5 |-"yoda" <= compare # SIM300
|
||||
5 |+compare >= "yoda" # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
|
||||
SIM300.py:7:1: SIM300 [*] Yoda conditions are discouraged, use `compare > "yoda"` instead
|
||||
SIM300.py:6:1: SIM300 [*] Yoda conditions are discouraged, use `compare > "yoda"` instead
|
||||
|
|
||||
5 | ("a", "b") == compare # SIM300
|
||||
6 | "yoda" <= compare # SIM300
|
||||
7 | "yoda" < compare # SIM300
|
||||
4 | ("a", "b") == compare # SIM300
|
||||
5 | "yoda" <= compare # SIM300
|
||||
6 | "yoda" < compare # SIM300
|
||||
| ^^^^^^^^^^^^^^^^ SIM300
|
||||
8 | 42 > age # SIM300
|
||||
9 | -42 > age # SIM300
|
||||
7 | 42 > age # SIM300
|
||||
8 | -42 > age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `compare > "yoda"`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | 42 == age # SIM300
|
||||
5 5 | ("a", "b") == compare # SIM300
|
||||
6 6 | "yoda" <= compare # SIM300
|
||||
7 |-"yoda" < compare # SIM300
|
||||
7 |+compare > "yoda" # SIM300
|
||||
8 8 | 42 > age # SIM300
|
||||
9 9 | -42 > age # SIM300
|
||||
10 10 | +42 > age # SIM300
|
||||
3 3 | 42 == age # SIM300
|
||||
4 4 | ("a", "b") == compare # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
6 |-"yoda" < compare # SIM300
|
||||
6 |+compare > "yoda" # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
|
||||
SIM300.py:8:1: SIM300 [*] Yoda conditions are discouraged, use `age < 42` instead
|
||||
|
|
||||
6 | "yoda" <= compare # SIM300
|
||||
7 | "yoda" < compare # SIM300
|
||||
8 | 42 > age # SIM300
|
||||
| ^^^^^^^^ SIM300
|
||||
9 | -42 > age # SIM300
|
||||
10 | +42 > age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age < 42`
|
||||
SIM300.py:7:1: SIM300 [*] Yoda conditions are discouraged, use `age < 42` instead
|
||||
|
|
||||
5 | "yoda" <= compare # SIM300
|
||||
6 | "yoda" < compare # SIM300
|
||||
7 | 42 > age # SIM300
|
||||
| ^^^^^^^^ SIM300
|
||||
8 | -42 > age # SIM300
|
||||
9 | +42 > age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age < 42`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | ("a", "b") == compare # SIM300
|
||||
6 6 | "yoda" <= compare # SIM300
|
||||
7 7 | "yoda" < compare # SIM300
|
||||
8 |-42 > age # SIM300
|
||||
8 |+age < 42 # SIM300
|
||||
9 9 | -42 > age # SIM300
|
||||
10 10 | +42 > age # SIM300
|
||||
11 11 | YODA == age # SIM300
|
||||
4 4 | ("a", "b") == compare # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
7 |-42 > age # SIM300
|
||||
7 |+age < 42 # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
10 10 | YODA == age # SIM300
|
||||
|
||||
SIM300.py:9:1: SIM300 [*] Yoda conditions are discouraged, use `age < -42` instead
|
||||
SIM300.py:8:1: SIM300 [*] Yoda conditions are discouraged, use `age < -42` instead
|
||||
|
|
||||
7 | "yoda" < compare # SIM300
|
||||
8 | 42 > age # SIM300
|
||||
9 | -42 > age # SIM300
|
||||
6 | "yoda" < compare # SIM300
|
||||
7 | 42 > age # SIM300
|
||||
8 | -42 > age # SIM300
|
||||
| ^^^^^^^^^ SIM300
|
||||
10 | +42 > age # SIM300
|
||||
11 | YODA == age # SIM300
|
||||
9 | +42 > age # SIM300
|
||||
10 | YODA == age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age < -42`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | "yoda" <= compare # SIM300
|
||||
7 7 | "yoda" < compare # SIM300
|
||||
8 8 | 42 > age # SIM300
|
||||
9 |--42 > age # SIM300
|
||||
9 |+age < -42 # SIM300
|
||||
10 10 | +42 > age # SIM300
|
||||
11 11 | YODA == age # SIM300
|
||||
12 12 | YODA > age # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
8 |--42 > age # SIM300
|
||||
8 |+age < -42 # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
10 10 | YODA == age # SIM300
|
||||
11 11 | YODA > age # SIM300
|
||||
|
||||
SIM300.py:10:1: SIM300 [*] Yoda conditions are discouraged, use `age < +42` instead
|
||||
SIM300.py:9:1: SIM300 [*] Yoda conditions are discouraged, use `age < +42` instead
|
||||
|
|
||||
8 | 42 > age # SIM300
|
||||
9 | -42 > age # SIM300
|
||||
10 | +42 > age # SIM300
|
||||
7 | 42 > age # SIM300
|
||||
8 | -42 > age # SIM300
|
||||
9 | +42 > age # SIM300
|
||||
| ^^^^^^^^^ SIM300
|
||||
11 | YODA == age # SIM300
|
||||
12 | YODA > age # SIM300
|
||||
10 | YODA == age # SIM300
|
||||
11 | YODA > age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age < +42`
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 | "yoda" < compare # SIM300
|
||||
8 8 | 42 > age # SIM300
|
||||
9 9 | -42 > age # SIM300
|
||||
10 |-+42 > age # SIM300
|
||||
10 |+age < +42 # SIM300
|
||||
11 11 | YODA == age # SIM300
|
||||
12 12 | YODA > age # SIM300
|
||||
13 13 | YODA >= age # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
9 |-+42 > age # SIM300
|
||||
9 |+age < +42 # SIM300
|
||||
10 10 | YODA == age # SIM300
|
||||
11 11 | YODA > age # SIM300
|
||||
12 12 | YODA >= age # SIM300
|
||||
|
||||
SIM300.py:11:1: SIM300 [*] Yoda conditions are discouraged, use `age == YODA` instead
|
||||
SIM300.py:10:1: SIM300 [*] Yoda conditions are discouraged, use `age == YODA` instead
|
||||
|
|
||||
9 | -42 > age # SIM300
|
||||
10 | +42 > age # SIM300
|
||||
11 | YODA == age # SIM300
|
||||
8 | -42 > age # SIM300
|
||||
9 | +42 > age # SIM300
|
||||
10 | YODA == age # SIM300
|
||||
| ^^^^^^^^^^^ SIM300
|
||||
12 | YODA > age # SIM300
|
||||
13 | YODA >= age # SIM300
|
||||
11 | YODA > age # SIM300
|
||||
12 | YODA >= age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age == YODA`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | 42 > age # SIM300
|
||||
9 9 | -42 > age # SIM300
|
||||
10 10 | +42 > age # SIM300
|
||||
11 |-YODA == age # SIM300
|
||||
11 |+age == YODA # SIM300
|
||||
12 12 | YODA > age # SIM300
|
||||
13 13 | YODA >= age # SIM300
|
||||
14 14 | JediOrder.YODA == age # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
10 |-YODA == age # SIM300
|
||||
10 |+age == YODA # SIM300
|
||||
11 11 | YODA > age # SIM300
|
||||
12 12 | YODA >= age # SIM300
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
|
||||
SIM300.py:12:1: SIM300 [*] Yoda conditions are discouraged, use `age < YODA` instead
|
||||
SIM300.py:11:1: SIM300 [*] Yoda conditions are discouraged, use `age < YODA` instead
|
||||
|
|
||||
10 | +42 > age # SIM300
|
||||
11 | YODA == age # SIM300
|
||||
12 | YODA > age # SIM300
|
||||
9 | +42 > age # SIM300
|
||||
10 | YODA == age # SIM300
|
||||
11 | YODA > age # SIM300
|
||||
| ^^^^^^^^^^ SIM300
|
||||
13 | YODA >= age # SIM300
|
||||
14 | JediOrder.YODA == age # SIM300
|
||||
12 | YODA >= age # SIM300
|
||||
13 | JediOrder.YODA == age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age < YODA`
|
||||
|
||||
ℹ Safe fix
|
||||
9 9 | -42 > age # SIM300
|
||||
10 10 | +42 > age # SIM300
|
||||
11 11 | YODA == age # SIM300
|
||||
12 |-YODA > age # SIM300
|
||||
12 |+age < YODA # SIM300
|
||||
13 13 | YODA >= age # SIM300
|
||||
14 14 | JediOrder.YODA == age # SIM300
|
||||
15 15 | 0 < (number - 100) # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
10 10 | YODA == age # SIM300
|
||||
11 |-YODA > age # SIM300
|
||||
11 |+age < YODA # SIM300
|
||||
12 12 | YODA >= age # SIM300
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
14 14 | 0 < (number - 100) # SIM300
|
||||
|
||||
SIM300.py:13:1: SIM300 [*] Yoda conditions are discouraged, use `age <= YODA` instead
|
||||
SIM300.py:12:1: SIM300 [*] Yoda conditions are discouraged, use `age <= YODA` instead
|
||||
|
|
||||
11 | YODA == age # SIM300
|
||||
12 | YODA > age # SIM300
|
||||
13 | YODA >= age # SIM300
|
||||
10 | YODA == age # SIM300
|
||||
11 | YODA > age # SIM300
|
||||
12 | YODA >= age # SIM300
|
||||
| ^^^^^^^^^^^ SIM300
|
||||
14 | JediOrder.YODA == age # SIM300
|
||||
15 | 0 < (number - 100) # SIM300
|
||||
13 | JediOrder.YODA == age # SIM300
|
||||
14 | 0 < (number - 100) # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age <= YODA`
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 | +42 > age # SIM300
|
||||
11 11 | YODA == age # SIM300
|
||||
12 12 | YODA > age # SIM300
|
||||
13 |-YODA >= age # SIM300
|
||||
13 |+age <= YODA # SIM300
|
||||
14 14 | JediOrder.YODA == age # SIM300
|
||||
15 15 | 0 < (number - 100) # SIM300
|
||||
16 16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
10 10 | YODA == age # SIM300
|
||||
11 11 | YODA > age # SIM300
|
||||
12 |-YODA >= age # SIM300
|
||||
12 |+age <= YODA # SIM300
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
14 14 | 0 < (number - 100) # SIM300
|
||||
15 15 | B<A[0][0]or B
|
||||
|
||||
SIM300.py:14:1: SIM300 [*] Yoda conditions are discouraged, use `age == JediOrder.YODA` instead
|
||||
SIM300.py:13:1: SIM300 [*] Yoda conditions are discouraged, use `age == JediOrder.YODA` instead
|
||||
|
|
||||
12 | YODA > age # SIM300
|
||||
13 | YODA >= age # SIM300
|
||||
14 | JediOrder.YODA == age # SIM300
|
||||
11 | YODA > age # SIM300
|
||||
12 | YODA >= age # SIM300
|
||||
13 | JediOrder.YODA == age # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ SIM300
|
||||
15 | 0 < (number - 100) # SIM300
|
||||
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
14 | 0 < (number - 100) # SIM300
|
||||
15 | B<A[0][0]or B
|
||||
|
|
||||
= help: Replace Yoda condition with `age == JediOrder.YODA`
|
||||
|
||||
ℹ Safe fix
|
||||
11 11 | YODA == age # SIM300
|
||||
12 12 | YODA > age # SIM300
|
||||
13 13 | YODA >= age # SIM300
|
||||
14 |-JediOrder.YODA == age # SIM300
|
||||
14 |+age == JediOrder.YODA # SIM300
|
||||
15 15 | 0 < (number - 100) # SIM300
|
||||
16 16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
17 17 | B<A[0][0]or B
|
||||
10 10 | YODA == age # SIM300
|
||||
11 11 | YODA > age # SIM300
|
||||
12 12 | YODA >= age # SIM300
|
||||
13 |-JediOrder.YODA == age # SIM300
|
||||
13 |+age == JediOrder.YODA # SIM300
|
||||
14 14 | 0 < (number - 100) # SIM300
|
||||
15 15 | B<A[0][0]or B
|
||||
16 16 | B or(B)<A[0][0]
|
||||
|
||||
SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100) > 0` instead
|
||||
SIM300.py:14:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100) > 0` instead
|
||||
|
|
||||
13 | YODA >= age # SIM300
|
||||
14 | JediOrder.YODA == age # SIM300
|
||||
15 | 0 < (number - 100) # SIM300
|
||||
12 | YODA >= age # SIM300
|
||||
13 | JediOrder.YODA == age # SIM300
|
||||
14 | 0 < (number - 100) # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^^ SIM300
|
||||
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
17 | B<A[0][0]or B
|
||||
15 | B<A[0][0]or B
|
||||
16 | B or(B)<A[0][0]
|
||||
|
|
||||
= help: Replace Yoda condition with `(number - 100) > 0`
|
||||
|
||||
ℹ Safe fix
|
||||
12 12 | YODA > age # SIM300
|
||||
13 13 | YODA >= age # SIM300
|
||||
14 14 | JediOrder.YODA == age # SIM300
|
||||
15 |-0 < (number - 100) # SIM300
|
||||
15 |+(number - 100) > 0 # SIM300
|
||||
16 16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
17 17 | B<A[0][0]or B
|
||||
18 18 | B or(B)<A[0][0]
|
||||
11 11 | YODA > age # SIM300
|
||||
12 12 | YODA >= age # SIM300
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
14 |-0 < (number - 100) # SIM300
|
||||
14 |+(number - 100) > 0 # SIM300
|
||||
15 15 | B<A[0][0]or B
|
||||
16 16 | B or(B)<A[0][0]
|
||||
17 17 |
|
||||
|
||||
SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged
|
||||
SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > B` instead
|
||||
|
|
||||
14 | JediOrder.YODA == age # SIM300
|
||||
15 | 0 < (number - 100) # SIM300
|
||||
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM300
|
||||
17 | B<A[0][0]or B
|
||||
18 | B or(B)<A[0][0]
|
||||
|
|
||||
= help: Replace Yoda condition
|
||||
|
||||
ℹ Safe fix
|
||||
13 13 | YODA >= age # SIM300
|
||||
14 14 | JediOrder.YODA == age # SIM300
|
||||
15 15 | 0 < (number - 100) # SIM300
|
||||
16 |-SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
16 |+(60 * 60) < SomeClass().settings.SOME_CONSTANT_VALUE # SIM300
|
||||
17 17 | B<A[0][0]or B
|
||||
18 18 | B or(B)<A[0][0]
|
||||
19 19 |
|
||||
|
||||
SIM300.py:17:1: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > B` instead
|
||||
|
|
||||
15 | 0 < (number - 100) # SIM300
|
||||
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
17 | B<A[0][0]or B
|
||||
13 | JediOrder.YODA == age # SIM300
|
||||
14 | 0 < (number - 100) # SIM300
|
||||
15 | B<A[0][0]or B
|
||||
| ^^^^^^^^^ SIM300
|
||||
18 | B or(B)<A[0][0]
|
||||
16 | B or(B)<A[0][0]
|
||||
|
|
||||
= help: Replace Yoda condition with `A[0][0] > B`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 | JediOrder.YODA == age # SIM300
|
||||
15 15 | 0 < (number - 100) # SIM300
|
||||
16 16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
17 |-B<A[0][0]or B
|
||||
17 |+A[0][0] > B or B
|
||||
18 18 | B or(B)<A[0][0]
|
||||
19 19 |
|
||||
20 20 | # OK
|
||||
12 12 | YODA >= age # SIM300
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
14 14 | 0 < (number - 100) # SIM300
|
||||
15 |-B<A[0][0]or B
|
||||
15 |+A[0][0] > B or B
|
||||
16 16 | B or(B)<A[0][0]
|
||||
17 17 |
|
||||
18 18 | # Errors in preview
|
||||
|
||||
SIM300.py:18:5: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > (B)` instead
|
||||
SIM300.py:16:5: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > (B)` instead
|
||||
|
|
||||
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
17 | B<A[0][0]or B
|
||||
18 | B or(B)<A[0][0]
|
||||
14 | 0 < (number - 100) # SIM300
|
||||
15 | B<A[0][0]or B
|
||||
16 | B or(B)<A[0][0]
|
||||
| ^^^^^^^^^^^ SIM300
|
||||
19 |
|
||||
20 | # OK
|
||||
17 |
|
||||
18 | # Errors in preview
|
||||
|
|
||||
= help: Replace Yoda condition with `A[0][0] > (B)`
|
||||
|
||||
ℹ Safe fix
|
||||
15 15 | 0 < (number - 100) # SIM300
|
||||
16 16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
||||
17 17 | B<A[0][0]or B
|
||||
18 |-B or(B)<A[0][0]
|
||||
18 |+B or A[0][0] > (B)
|
||||
19 19 |
|
||||
20 20 | # OK
|
||||
21 21 | compare == "yoda"
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
14 14 | 0 < (number - 100) # SIM300
|
||||
15 15 | B<A[0][0]or B
|
||||
16 |-B or(B)<A[0][0]
|
||||
16 |+B or A[0][0] > (B)
|
||||
17 17 |
|
||||
18 18 | # Errors in preview
|
||||
19 19 | ['upper'] == UPPER_LIST
|
||||
|
||||
SIM300.py:23:1: SIM300 [*] Yoda conditions are discouraged, use `['upper'] == UPPER_LIST` instead
|
||||
|
|
||||
22 | # Errors in stable
|
||||
23 | UPPER_LIST == ['upper']
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM300
|
||||
24 | DummyHandler.CONFIG == {}
|
||||
|
|
||||
= help: Replace Yoda condition with `['upper'] == UPPER_LIST`
|
||||
|
||||
ℹ Safe fix
|
||||
20 20 | {} == DummyHandler.CONFIG
|
||||
21 21 |
|
||||
22 22 | # Errors in stable
|
||||
23 |-UPPER_LIST == ['upper']
|
||||
23 |+['upper'] == UPPER_LIST
|
||||
24 24 | DummyHandler.CONFIG == {}
|
||||
25 25 |
|
||||
26 26 | # OK
|
||||
|
||||
SIM300.py:24:1: SIM300 [*] Yoda conditions are discouraged, use `{} == DummyHandler.CONFIG` instead
|
||||
|
|
||||
22 | # Errors in stable
|
||||
23 | UPPER_LIST == ['upper']
|
||||
24 | DummyHandler.CONFIG == {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM300
|
||||
25 |
|
||||
26 | # OK
|
||||
|
|
||||
= help: Replace Yoda condition with `{} == DummyHandler.CONFIG`
|
||||
|
||||
ℹ Safe fix
|
||||
21 21 |
|
||||
22 22 | # Errors in stable
|
||||
23 23 | UPPER_LIST == ['upper']
|
||||
24 |-DummyHandler.CONFIG == {}
|
||||
24 |+{} == DummyHandler.CONFIG
|
||||
25 25 |
|
||||
26 26 | # OK
|
||||
27 27 | compare == "yoda"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM300.py:2:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda"` instead
|
||||
|
|
||||
1 | # Errors
|
||||
2 | "yoda" == compare # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^ SIM300
|
||||
3 | 42 == age # SIM300
|
||||
4 | ("a", "b") == compare # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `compare == "yoda"`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |-"yoda" == compare # SIM300
|
||||
2 |+compare == "yoda" # SIM300
|
||||
3 3 | 42 == age # SIM300
|
||||
4 4 | ("a", "b") == compare # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
|
||||
SIM300.py:3:1: SIM300 [*] Yoda conditions are discouraged, use `age == 42` instead
|
||||
|
|
||||
1 | # Errors
|
||||
2 | "yoda" == compare # SIM300
|
||||
3 | 42 == age # SIM300
|
||||
| ^^^^^^^^^ SIM300
|
||||
4 | ("a", "b") == compare # SIM300
|
||||
5 | "yoda" <= compare # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age == 42`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 2 | "yoda" == compare # SIM300
|
||||
3 |-42 == age # SIM300
|
||||
3 |+age == 42 # SIM300
|
||||
4 4 | ("a", "b") == compare # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
|
||||
SIM300.py:4:1: SIM300 [*] Yoda conditions are discouraged, use `compare == ("a", "b")` instead
|
||||
|
|
||||
2 | "yoda" == compare # SIM300
|
||||
3 | 42 == age # SIM300
|
||||
4 | ("a", "b") == compare # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ SIM300
|
||||
5 | "yoda" <= compare # SIM300
|
||||
6 | "yoda" < compare # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `compare == ("a", "b")`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 2 | "yoda" == compare # SIM300
|
||||
3 3 | 42 == age # SIM300
|
||||
4 |-("a", "b") == compare # SIM300
|
||||
4 |+compare == ("a", "b") # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
|
||||
SIM300.py:5:1: SIM300 [*] Yoda conditions are discouraged, use `compare >= "yoda"` instead
|
||||
|
|
||||
3 | 42 == age # SIM300
|
||||
4 | ("a", "b") == compare # SIM300
|
||||
5 | "yoda" <= compare # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^ SIM300
|
||||
6 | "yoda" < compare # SIM300
|
||||
7 | 42 > age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `compare >= "yoda"`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | "yoda" == compare # SIM300
|
||||
3 3 | 42 == age # SIM300
|
||||
4 4 | ("a", "b") == compare # SIM300
|
||||
5 |-"yoda" <= compare # SIM300
|
||||
5 |+compare >= "yoda" # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
|
||||
SIM300.py:6:1: SIM300 [*] Yoda conditions are discouraged, use `compare > "yoda"` instead
|
||||
|
|
||||
4 | ("a", "b") == compare # SIM300
|
||||
5 | "yoda" <= compare # SIM300
|
||||
6 | "yoda" < compare # SIM300
|
||||
| ^^^^^^^^^^^^^^^^ SIM300
|
||||
7 | 42 > age # SIM300
|
||||
8 | -42 > age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `compare > "yoda"`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | 42 == age # SIM300
|
||||
4 4 | ("a", "b") == compare # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
6 |-"yoda" < compare # SIM300
|
||||
6 |+compare > "yoda" # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
|
||||
SIM300.py:7:1: SIM300 [*] Yoda conditions are discouraged, use `age < 42` instead
|
||||
|
|
||||
5 | "yoda" <= compare # SIM300
|
||||
6 | "yoda" < compare # SIM300
|
||||
7 | 42 > age # SIM300
|
||||
| ^^^^^^^^ SIM300
|
||||
8 | -42 > age # SIM300
|
||||
9 | +42 > age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age < 42`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | ("a", "b") == compare # SIM300
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
7 |-42 > age # SIM300
|
||||
7 |+age < 42 # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
10 10 | YODA == age # SIM300
|
||||
|
||||
SIM300.py:8:1: SIM300 [*] Yoda conditions are discouraged, use `age < -42` instead
|
||||
|
|
||||
6 | "yoda" < compare # SIM300
|
||||
7 | 42 > age # SIM300
|
||||
8 | -42 > age # SIM300
|
||||
| ^^^^^^^^^ SIM300
|
||||
9 | +42 > age # SIM300
|
||||
10 | YODA == age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age < -42`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | "yoda" <= compare # SIM300
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
8 |--42 > age # SIM300
|
||||
8 |+age < -42 # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
10 10 | YODA == age # SIM300
|
||||
11 11 | YODA > age # SIM300
|
||||
|
||||
SIM300.py:9:1: SIM300 [*] Yoda conditions are discouraged, use `age < +42` instead
|
||||
|
|
||||
7 | 42 > age # SIM300
|
||||
8 | -42 > age # SIM300
|
||||
9 | +42 > age # SIM300
|
||||
| ^^^^^^^^^ SIM300
|
||||
10 | YODA == age # SIM300
|
||||
11 | YODA > age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age < +42`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | "yoda" < compare # SIM300
|
||||
7 7 | 42 > age # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
9 |-+42 > age # SIM300
|
||||
9 |+age < +42 # SIM300
|
||||
10 10 | YODA == age # SIM300
|
||||
11 11 | YODA > age # SIM300
|
||||
12 12 | YODA >= age # SIM300
|
||||
|
||||
SIM300.py:10:1: SIM300 [*] Yoda conditions are discouraged, use `age == YODA` instead
|
||||
|
|
||||
8 | -42 > age # SIM300
|
||||
9 | +42 > age # SIM300
|
||||
10 | YODA == age # SIM300
|
||||
| ^^^^^^^^^^^ SIM300
|
||||
11 | YODA > age # SIM300
|
||||
12 | YODA >= age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age == YODA`
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 | 42 > age # SIM300
|
||||
8 8 | -42 > age # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
10 |-YODA == age # SIM300
|
||||
10 |+age == YODA # SIM300
|
||||
11 11 | YODA > age # SIM300
|
||||
12 12 | YODA >= age # SIM300
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
|
||||
SIM300.py:11:1: SIM300 [*] Yoda conditions are discouraged, use `age < YODA` instead
|
||||
|
|
||||
9 | +42 > age # SIM300
|
||||
10 | YODA == age # SIM300
|
||||
11 | YODA > age # SIM300
|
||||
| ^^^^^^^^^^ SIM300
|
||||
12 | YODA >= age # SIM300
|
||||
13 | JediOrder.YODA == age # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age < YODA`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | -42 > age # SIM300
|
||||
9 9 | +42 > age # SIM300
|
||||
10 10 | YODA == age # SIM300
|
||||
11 |-YODA > age # SIM300
|
||||
11 |+age < YODA # SIM300
|
||||
12 12 | YODA >= age # SIM300
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
14 14 | 0 < (number - 100) # SIM300
|
||||
|
||||
SIM300.py:12:1: SIM300 [*] Yoda conditions are discouraged, use `age <= YODA` instead
|
||||
|
|
||||
10 | YODA == age # SIM300
|
||||
11 | YODA > age # SIM300
|
||||
12 | YODA >= age # SIM300
|
||||
| ^^^^^^^^^^^ SIM300
|
||||
13 | JediOrder.YODA == age # SIM300
|
||||
14 | 0 < (number - 100) # SIM300
|
||||
|
|
||||
= help: Replace Yoda condition with `age <= YODA`
|
||||
|
||||
ℹ Safe fix
|
||||
9 9 | +42 > age # SIM300
|
||||
10 10 | YODA == age # SIM300
|
||||
11 11 | YODA > age # SIM300
|
||||
12 |-YODA >= age # SIM300
|
||||
12 |+age <= YODA # SIM300
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
14 14 | 0 < (number - 100) # SIM300
|
||||
15 15 | B<A[0][0]or B
|
||||
|
||||
SIM300.py:13:1: SIM300 [*] Yoda conditions are discouraged, use `age == JediOrder.YODA` instead
|
||||
|
|
||||
11 | YODA > age # SIM300
|
||||
12 | YODA >= age # SIM300
|
||||
13 | JediOrder.YODA == age # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ SIM300
|
||||
14 | 0 < (number - 100) # SIM300
|
||||
15 | B<A[0][0]or B
|
||||
|
|
||||
= help: Replace Yoda condition with `age == JediOrder.YODA`
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 | YODA == age # SIM300
|
||||
11 11 | YODA > age # SIM300
|
||||
12 12 | YODA >= age # SIM300
|
||||
13 |-JediOrder.YODA == age # SIM300
|
||||
13 |+age == JediOrder.YODA # SIM300
|
||||
14 14 | 0 < (number - 100) # SIM300
|
||||
15 15 | B<A[0][0]or B
|
||||
16 16 | B or(B)<A[0][0]
|
||||
|
||||
SIM300.py:14:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100) > 0` instead
|
||||
|
|
||||
12 | YODA >= age # SIM300
|
||||
13 | JediOrder.YODA == age # SIM300
|
||||
14 | 0 < (number - 100) # SIM300
|
||||
| ^^^^^^^^^^^^^^^^^^ SIM300
|
||||
15 | B<A[0][0]or B
|
||||
16 | B or(B)<A[0][0]
|
||||
|
|
||||
= help: Replace Yoda condition with `(number - 100) > 0`
|
||||
|
||||
ℹ Safe fix
|
||||
11 11 | YODA > age # SIM300
|
||||
12 12 | YODA >= age # SIM300
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
14 |-0 < (number - 100) # SIM300
|
||||
14 |+(number - 100) > 0 # SIM300
|
||||
15 15 | B<A[0][0]or B
|
||||
16 16 | B or(B)<A[0][0]
|
||||
17 17 |
|
||||
|
||||
SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > B` instead
|
||||
|
|
||||
13 | JediOrder.YODA == age # SIM300
|
||||
14 | 0 < (number - 100) # SIM300
|
||||
15 | B<A[0][0]or B
|
||||
| ^^^^^^^^^ SIM300
|
||||
16 | B or(B)<A[0][0]
|
||||
|
|
||||
= help: Replace Yoda condition with `A[0][0] > B`
|
||||
|
||||
ℹ Safe fix
|
||||
12 12 | YODA >= age # SIM300
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
14 14 | 0 < (number - 100) # SIM300
|
||||
15 |-B<A[0][0]or B
|
||||
15 |+A[0][0] > B or B
|
||||
16 16 | B or(B)<A[0][0]
|
||||
17 17 |
|
||||
18 18 | # Errors in preview
|
||||
|
||||
SIM300.py:16:5: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > (B)` instead
|
||||
|
|
||||
14 | 0 < (number - 100) # SIM300
|
||||
15 | B<A[0][0]or B
|
||||
16 | B or(B)<A[0][0]
|
||||
| ^^^^^^^^^^^ SIM300
|
||||
17 |
|
||||
18 | # Errors in preview
|
||||
|
|
||||
= help: Replace Yoda condition with `A[0][0] > (B)`
|
||||
|
||||
ℹ Safe fix
|
||||
13 13 | JediOrder.YODA == age # SIM300
|
||||
14 14 | 0 < (number - 100) # SIM300
|
||||
15 15 | B<A[0][0]or B
|
||||
16 |-B or(B)<A[0][0]
|
||||
16 |+B or A[0][0] > (B)
|
||||
17 17 |
|
||||
18 18 | # Errors in preview
|
||||
19 19 | ['upper'] == UPPER_LIST
|
||||
|
||||
SIM300.py:19:1: SIM300 [*] Yoda conditions are discouraged, use `UPPER_LIST == ['upper']` instead
|
||||
|
|
||||
18 | # Errors in preview
|
||||
19 | ['upper'] == UPPER_LIST
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM300
|
||||
20 | {} == DummyHandler.CONFIG
|
||||
|
|
||||
= help: Replace Yoda condition with `UPPER_LIST == ['upper']`
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 | B or(B)<A[0][0]
|
||||
17 17 |
|
||||
18 18 | # Errors in preview
|
||||
19 |-['upper'] == UPPER_LIST
|
||||
19 |+UPPER_LIST == ['upper']
|
||||
20 20 | {} == DummyHandler.CONFIG
|
||||
21 21 |
|
||||
22 22 | # Errors in stable
|
||||
|
||||
SIM300.py:20:1: SIM300 [*] Yoda conditions are discouraged, use `DummyHandler.CONFIG == {}` instead
|
||||
|
|
||||
18 | # Errors in preview
|
||||
19 | ['upper'] == UPPER_LIST
|
||||
20 | {} == DummyHandler.CONFIG
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM300
|
||||
21 |
|
||||
22 | # Errors in stable
|
||||
|
|
||||
= help: Replace Yoda condition with `DummyHandler.CONFIG == {}`
|
||||
|
||||
ℹ Safe fix
|
||||
17 17 |
|
||||
18 18 | # Errors in preview
|
||||
19 19 | ['upper'] == UPPER_LIST
|
||||
20 |-{} == DummyHandler.CONFIG
|
||||
20 |+DummyHandler.CONFIG == {}
|
||||
21 21 |
|
||||
22 22 | # Errors in stable
|
||||
23 23 | UPPER_LIST == ['upper']
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use anyhow::Result;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::call_path::from_qualified_name;
|
||||
use ruff_python_ast::helpers::{map_callable, map_subscript};
|
||||
use ruff_python_ast::helpers::map_callable;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_codegen::{Generator, Stylist};
|
||||
use ruff_python_semantic::{
|
||||
Binding, BindingId, BindingKind, NodeId, ResolvedReference, SemanticModel,
|
||||
analyze, Binding, BindingKind, NodeId, ResolvedReference, SemanticModel,
|
||||
};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -59,57 +58,17 @@ pub(crate) fn runtime_required_class(
|
||||
false
|
||||
}
|
||||
|
||||
/// Return `true` if a class is a subclass of a runtime-required base class.
|
||||
fn runtime_required_base_class(
|
||||
class_def: &ast::StmtClassDef,
|
||||
base_classes: &[String],
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
fn inner(
|
||||
class_def: &ast::StmtClassDef,
|
||||
base_classes: &[String],
|
||||
semantic: &SemanticModel,
|
||||
seen: &mut FxHashSet<BindingId>,
|
||||
) -> bool {
|
||||
class_def.bases().iter().any(|expr| {
|
||||
// If the base class is itself runtime-required, then this is too.
|
||||
// Ex) `class Foo(BaseModel): ...`
|
||||
if semantic
|
||||
.resolve_call_path(map_subscript(expr))
|
||||
.is_some_and(|call_path| {
|
||||
base_classes
|
||||
.iter()
|
||||
.any(|base_class| from_qualified_name(base_class) == call_path)
|
||||
})
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the base class extends a runtime-required class, then this does too.
|
||||
// Ex) `class Bar(BaseModel): ...; class Foo(Bar): ...`
|
||||
if let Some(id) = semantic.lookup_attribute(map_subscript(expr)) {
|
||||
if seen.insert(id) {
|
||||
let binding = semantic.binding(id);
|
||||
if let Some(base_class) = binding
|
||||
.kind
|
||||
.as_class_definition()
|
||||
.map(|id| &semantic.scopes[*id])
|
||||
.and_then(|scope| scope.kind.as_class())
|
||||
{
|
||||
if inner(base_class, base_classes, semantic, seen) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
if base_classes.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
inner(class_def, base_classes, semantic, &mut FxHashSet::default())
|
||||
analyze::class::any_over_body(class_def, semantic, &|call_path| {
|
||||
base_classes
|
||||
.iter()
|
||||
.any(|base_class| from_qualified_name(base_class) == call_path)
|
||||
})
|
||||
}
|
||||
|
||||
fn runtime_required_decorators(
|
||||
@@ -215,6 +174,7 @@ pub(crate) fn quote_annotation(
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
generator: Generator,
|
||||
) -> Result<Edit> {
|
||||
let expr = semantic.expression(node_id).expect("Expression not found");
|
||||
if let Some(parent_id) = semantic.parent_expression_id(node_id) {
|
||||
@@ -224,7 +184,7 @@ pub(crate) fn quote_annotation(
|
||||
// If we're quoting the value of a subscript, we need to quote the entire
|
||||
// expression. For example, when quoting `DataFrame` in `DataFrame[int]`, we
|
||||
// should generate `"DataFrame[int]"`.
|
||||
return quote_annotation(parent_id, semantic, locator, stylist);
|
||||
return quote_annotation(parent_id, semantic, locator, stylist, generator);
|
||||
}
|
||||
}
|
||||
Some(Expr::Attribute(parent)) => {
|
||||
@@ -232,7 +192,7 @@ pub(crate) fn quote_annotation(
|
||||
// If we're quoting the value of an attribute, we need to quote the entire
|
||||
// expression. For example, when quoting `DataFrame` in `pd.DataFrame`, we
|
||||
// should generate `"pd.DataFrame"`.
|
||||
return quote_annotation(parent_id, semantic, locator, stylist);
|
||||
return quote_annotation(parent_id, semantic, locator, stylist, generator);
|
||||
}
|
||||
}
|
||||
Some(Expr::Call(parent)) => {
|
||||
@@ -240,7 +200,7 @@ pub(crate) fn quote_annotation(
|
||||
// If we're quoting the function of a call, we need to quote the entire
|
||||
// expression. For example, when quoting `DataFrame` in `DataFrame()`, we
|
||||
// should generate `"DataFrame()"`.
|
||||
return quote_annotation(parent_id, semantic, locator, stylist);
|
||||
return quote_annotation(parent_id, semantic, locator, stylist, generator);
|
||||
}
|
||||
}
|
||||
Some(Expr::BinOp(parent)) => {
|
||||
@@ -248,27 +208,44 @@ pub(crate) fn quote_annotation(
|
||||
// If we're quoting the left or right side of a binary operation, we need to
|
||||
// quote the entire expression. For example, when quoting `DataFrame` in
|
||||
// `DataFrame | Series`, we should generate `"DataFrame | Series"`.
|
||||
return quote_annotation(parent_id, semantic, locator, stylist);
|
||||
return quote_annotation(parent_id, semantic, locator, stylist, generator);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let annotation = locator.slice(expr);
|
||||
|
||||
// If the annotation already contains a quote, avoid attempting to re-quote it. For example:
|
||||
// ```python
|
||||
// from typing import Literal
|
||||
//
|
||||
// Set[Literal["Foo"]]
|
||||
// ```
|
||||
if annotation.contains('\'') || annotation.contains('"') {
|
||||
let text = locator.slice(expr);
|
||||
if text.contains('\'') || text.contains('"') {
|
||||
return Err(anyhow::anyhow!("Annotation already contains a quote"));
|
||||
}
|
||||
|
||||
// If we're quoting a name, we need to quote the entire expression.
|
||||
// Quote the entire expression.
|
||||
let quote = stylist.quote();
|
||||
let annotation = format!("{quote}{annotation}{quote}");
|
||||
Ok(Edit::range_replacement(annotation, expr.range()))
|
||||
let annotation = generator.expr(expr);
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
format!("{quote}{annotation}{quote}"),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Filter out any [`Edit`]s that are completely contained by any other [`Edit`].
|
||||
pub(crate) fn filter_contained(edits: Vec<Edit>) -> Vec<Edit> {
|
||||
let mut filtered: Vec<Edit> = Vec::with_capacity(edits.len());
|
||||
for edit in edits {
|
||||
if filtered
|
||||
.iter()
|
||||
.all(|filtered_edit| !filtered_edit.range().contains_range(edit.range()))
|
||||
{
|
||||
filtered.push(edit);
|
||||
}
|
||||
}
|
||||
filtered
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ mod tests {
|
||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_8.py"))]
|
||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_9.py"))]
|
||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))]
|
||||
#[test_case(Rule::RuntimeStringUnion, Path::new("TCH006_1.py"))]
|
||||
#[test_case(Rule::RuntimeStringUnion, Path::new("TCH006_2.py"))]
|
||||
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))]
|
||||
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
|
||||
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("snapshot.py"))]
|
||||
@@ -104,6 +106,35 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
Rule::TypingOnlyStandardLibraryImport,
|
||||
Path::new("exempt_type_checking_1.py")
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::TypingOnlyStandardLibraryImport,
|
||||
Path::new("exempt_type_checking_2.py")
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::TypingOnlyStandardLibraryImport,
|
||||
Path::new("exempt_type_checking_3.py")
|
||||
)]
|
||||
fn exempt_type_checking(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
flake8_type_checking: super::settings::Settings {
|
||||
exempt_modules: vec![],
|
||||
strict: true,
|
||||
..Default::default()
|
||||
},
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
Rule::RuntimeImportInTypeCheckingBlock,
|
||||
Path::new("runtime_evaluated_base_classes_1.py")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pub(crate) use empty_type_checking_block::*;
|
||||
pub(crate) use runtime_import_in_type_checking_block::*;
|
||||
pub(crate) use runtime_string_union::*;
|
||||
pub(crate) use typing_only_runtime_import::*;
|
||||
|
||||
mod empty_type_checking_block;
|
||||
mod runtime_import_in_type_checking_block;
|
||||
mod runtime_string_union;
|
||||
mod typing_only_runtime_import;
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::fix;
|
||||
use crate::importer::ImportedMembers;
|
||||
use crate::rules::flake8_type_checking::helpers::quote_annotation;
|
||||
use crate::rules::flake8_type_checking::helpers::{filter_contained, quote_annotation};
|
||||
use crate::rules::flake8_type_checking::imports::ImportBinding;
|
||||
|
||||
/// ## What it does
|
||||
@@ -262,32 +262,33 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
||||
|
||||
/// Generate a [`Fix`] to quote runtime usages for imports in a type-checking block.
|
||||
fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) -> Result<Fix> {
|
||||
let mut quote_reference_edits = imports
|
||||
.iter()
|
||||
.flat_map(|ImportBinding { binding, .. }| {
|
||||
binding.references.iter().filter_map(|reference_id| {
|
||||
let reference = checker.semantic().reference(*reference_id);
|
||||
if reference.context().is_runtime() {
|
||||
Some(quote_annotation(
|
||||
reference.expression_id()?,
|
||||
checker.semantic(),
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let quote_reference_edits = filter_contained(
|
||||
imports
|
||||
.iter()
|
||||
.flat_map(|ImportBinding { binding, .. }| {
|
||||
binding.references.iter().filter_map(|reference_id| {
|
||||
let reference = checker.semantic().reference(*reference_id);
|
||||
if reference.context().is_runtime() {
|
||||
Some(quote_annotation(
|
||||
reference.expression_id()?,
|
||||
checker.semantic(),
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.generator(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let quote_reference_edit = quote_reference_edits
|
||||
.pop()
|
||||
.expect("Expected at least one reference");
|
||||
Ok(
|
||||
Fix::unsafe_edits(quote_reference_edit, quote_reference_edits).isolate(Checker::isolation(
|
||||
checker.semantic().parent_statement_id(node_id),
|
||||
)),
|
||||
)
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
);
|
||||
|
||||
let mut rest = quote_reference_edits.into_iter();
|
||||
let head = rest.next().expect("Expected at least one reference");
|
||||
Ok(Fix::unsafe_edits(head, rest).isolate(Checker::isolation(
|
||||
checker.semantic().parent_statement_id(node_id),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Generate a [`Fix`] to remove runtime imports from a type-checking block.
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{Expr, Operator};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the presence of string literals in `X | Y`-style union types.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// [PEP 604] introduced a new syntax for union type annotations based on the
|
||||
/// `|` operator.
|
||||
///
|
||||
/// While Python's type annotations can typically be wrapped in strings to
|
||||
/// avoid runtime evaluation, the use of a string member within an `X | Y`-style
|
||||
/// union type will cause a runtime error.
|
||||
///
|
||||
/// Instead, remove the quotes, wrap the _entire_ union in quotes, or use
|
||||
/// `from __future__ import annotations` to disable runtime evaluation of
|
||||
/// annotations entirely.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// var: str | "int"
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// var: str | int
|
||||
/// ```
|
||||
///
|
||||
/// Or, extend the quotes to include the entire union:
|
||||
/// ```python
|
||||
/// var: "str | int"
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 535](https://peps.python.org/pep-0563/)
|
||||
/// - [PEP 604](https://peps.python.org/pep-0604/)
|
||||
///
|
||||
/// [PEP 604]: https://peps.python.org/pep-0604/
|
||||
#[violation]
|
||||
pub struct RuntimeStringUnion;
|
||||
|
||||
impl Violation for RuntimeStringUnion {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Invalid string member in `X | Y`-style union type")
|
||||
}
|
||||
}
|
||||
|
||||
/// TCH006
|
||||
pub(crate) fn runtime_string_union(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().in_type_definition() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker.semantic().execution_context().is_runtime() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for strings within the binary operator.
|
||||
let mut strings = Vec::new();
|
||||
traverse_op(expr, &mut strings);
|
||||
|
||||
for string in strings {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(RuntimeStringUnion, string.range()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect all string members in possibly-nested binary `|` expressions.
|
||||
fn traverse_op<'a>(expr: &'a Expr, strings: &mut Vec<&'a Expr>) {
|
||||
match expr {
|
||||
Expr::StringLiteral(_) => {
|
||||
strings.push(expr);
|
||||
}
|
||||
Expr::BytesLiteral(_) => {
|
||||
strings.push(expr);
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
right,
|
||||
op: Operator::BitOr,
|
||||
..
|
||||
}) => {
|
||||
traverse_op(left, strings);
|
||||
traverse_op(right, strings);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,9 @@ use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::fix;
|
||||
use crate::importer::ImportedMembers;
|
||||
use crate::rules::flake8_type_checking::helpers::{is_typing_reference, quote_annotation};
|
||||
use crate::rules::flake8_type_checking::helpers::{
|
||||
filter_contained, is_typing_reference, quote_annotation,
|
||||
};
|
||||
use crate::rules::flake8_type_checking::imports::ImportBinding;
|
||||
use crate::rules::isort::{categorize, ImportSection, ImportType};
|
||||
|
||||
@@ -471,41 +473,47 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
|
||||
)?;
|
||||
|
||||
// Step 2) Add the import to a `TYPE_CHECKING` block.
|
||||
let add_import_edit = checker.importer().typing_import_edit(
|
||||
&ImportedMembers {
|
||||
statement,
|
||||
names: member_names.iter().map(AsRef::as_ref).collect(),
|
||||
},
|
||||
at,
|
||||
checker.semantic(),
|
||||
checker.source_type,
|
||||
)?;
|
||||
let (type_checking_edit, add_import_edit) = checker
|
||||
.importer()
|
||||
.typing_import_edit(
|
||||
&ImportedMembers {
|
||||
statement,
|
||||
names: member_names.iter().map(AsRef::as_ref).collect(),
|
||||
},
|
||||
at,
|
||||
checker.semantic(),
|
||||
checker.source_type,
|
||||
)?
|
||||
.into_edits();
|
||||
|
||||
// Step 3) Quote any runtime usages of the referenced symbol.
|
||||
let quote_reference_edits = imports
|
||||
.iter()
|
||||
.flat_map(|ImportBinding { binding, .. }| {
|
||||
binding.references.iter().filter_map(|reference_id| {
|
||||
let reference = checker.semantic().reference(*reference_id);
|
||||
if reference.context().is_runtime() {
|
||||
Some(quote_annotation(
|
||||
reference.expression_id()?,
|
||||
checker.semantic(),
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let quote_reference_edits = filter_contained(
|
||||
imports
|
||||
.iter()
|
||||
.flat_map(|ImportBinding { binding, .. }| {
|
||||
binding.references.iter().filter_map(|reference_id| {
|
||||
let reference = checker.semantic().reference(*reference_id);
|
||||
if reference.context().is_runtime() {
|
||||
Some(quote_annotation(
|
||||
reference.expression_id()?,
|
||||
checker.semantic(),
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.generator(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
);
|
||||
|
||||
Ok(Fix::unsafe_edits(
|
||||
remove_import_edit,
|
||||
type_checking_edit,
|
||||
add_import_edit
|
||||
.into_edits()
|
||||
.into_iter()
|
||||
.chain(std::iter::once(remove_import_edit))
|
||||
.chain(quote_reference_edits),
|
||||
)
|
||||
.isolate(Checker::isolation(
|
||||
|
||||
@@ -18,5 +18,7 @@ quote.py:64:28: TCH004 [*] Quote references to `pandas.DataFrame`. Import is in
|
||||
66 |- def func(value: DataFrame):
|
||||
66 |+ def func(value: "DataFrame"):
|
||||
67 67 | ...
|
||||
68 68 |
|
||||
69 69 |
|
||||
|
||||
|
||||
|
||||
@@ -196,4 +196,146 @@ quote.py:54:24: TCH002 Move third-party import `pandas.DataFrame` into a type-ch
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
quote.py:71:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
||||
|
|
||||
70 | def f():
|
||||
71 | from pandas import DataFrame, Series
|
||||
| ^^^^^^^^^ TCH002
|
||||
72 |
|
||||
73 | def baz() -> DataFrame | Series:
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from typing import TYPE_CHECKING
|
||||
2 |+
|
||||
3 |+if TYPE_CHECKING:
|
||||
4 |+ from pandas import DataFrame, Series
|
||||
1 5 | def f():
|
||||
2 6 | from pandas import DataFrame
|
||||
3 7 |
|
||||
--------------------------------------------------------------------------------
|
||||
68 72 |
|
||||
69 73 |
|
||||
70 74 | def f():
|
||||
71 |- from pandas import DataFrame, Series
|
||||
72 75 |
|
||||
73 |- def baz() -> DataFrame | Series:
|
||||
76 |+ def baz() -> "DataFrame | Series":
|
||||
74 77 | ...
|
||||
75 78 |
|
||||
76 79 |
|
||||
|
||||
quote.py:71:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
|
||||
|
|
||||
70 | def f():
|
||||
71 | from pandas import DataFrame, Series
|
||||
| ^^^^^^ TCH002
|
||||
72 |
|
||||
73 | def baz() -> DataFrame | Series:
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from typing import TYPE_CHECKING
|
||||
2 |+
|
||||
3 |+if TYPE_CHECKING:
|
||||
4 |+ from pandas import DataFrame, Series
|
||||
1 5 | def f():
|
||||
2 6 | from pandas import DataFrame
|
||||
3 7 |
|
||||
--------------------------------------------------------------------------------
|
||||
68 72 |
|
||||
69 73 |
|
||||
70 74 | def f():
|
||||
71 |- from pandas import DataFrame, Series
|
||||
72 75 |
|
||||
73 |- def baz() -> DataFrame | Series:
|
||||
76 |+ def baz() -> "DataFrame | Series":
|
||||
74 77 | ...
|
||||
75 78 |
|
||||
76 79 |
|
||||
|
||||
quote.py:78:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
||||
|
|
||||
77 | def f():
|
||||
78 | from pandas import DataFrame, Series
|
||||
| ^^^^^^^^^ TCH002
|
||||
79 |
|
||||
80 | def baz() -> (
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from typing import TYPE_CHECKING
|
||||
2 |+
|
||||
3 |+if TYPE_CHECKING:
|
||||
4 |+ from pandas import DataFrame, Series
|
||||
1 5 | def f():
|
||||
2 6 | from pandas import DataFrame
|
||||
3 7 |
|
||||
--------------------------------------------------------------------------------
|
||||
75 79 |
|
||||
76 80 |
|
||||
77 81 | def f():
|
||||
78 |- from pandas import DataFrame, Series
|
||||
79 82 |
|
||||
80 83 | def baz() -> (
|
||||
81 |- DataFrame |
|
||||
82 |- Series
|
||||
84 |+ "DataFrame | Series"
|
||||
83 85 | ):
|
||||
84 86 | ...
|
||||
85 87 |
|
||||
86 88 | class C:
|
||||
87 |- x: DataFrame[
|
||||
88 |- int
|
||||
89 |- ] = 1
|
||||
89 |+ x: "DataFrame[int]" = 1
|
||||
90 90 |
|
||||
91 |- def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
|
||||
91 |+ def func() -> "DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]":
|
||||
92 92 | ...
|
||||
|
||||
quote.py:78:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
|
||||
|
|
||||
77 | def f():
|
||||
78 | from pandas import DataFrame, Series
|
||||
| ^^^^^^ TCH002
|
||||
79 |
|
||||
80 | def baz() -> (
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from typing import TYPE_CHECKING
|
||||
2 |+
|
||||
3 |+if TYPE_CHECKING:
|
||||
4 |+ from pandas import DataFrame, Series
|
||||
1 5 | def f():
|
||||
2 6 | from pandas import DataFrame
|
||||
3 7 |
|
||||
--------------------------------------------------------------------------------
|
||||
75 79 |
|
||||
76 80 |
|
||||
77 81 | def f():
|
||||
78 |- from pandas import DataFrame, Series
|
||||
79 82 |
|
||||
80 83 | def baz() -> (
|
||||
81 |- DataFrame |
|
||||
82 |- Series
|
||||
84 |+ "DataFrame | Series"
|
||||
83 85 | ):
|
||||
84 86 | ...
|
||||
85 87 |
|
||||
86 88 | class C:
|
||||
87 |- x: DataFrame[
|
||||
88 |- int
|
||||
89 |- ] = 1
|
||||
89 |+ x: "DataFrame[int]" = 1
|
||||
90 90 |
|
||||
91 |- def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
|
||||
91 |+ def func() -> "DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]":
|
||||
92 92 | ...
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
TCH006_1.py:18:30: TCH006 Invalid string member in `X | Y`-style union type
|
||||
|
|
||||
16 | type A = Value["int" | str] # OK
|
||||
17 |
|
||||
18 | OldS = TypeVar('OldS', int | 'str', str) # TCH006
|
||||
| ^^^^^ TCH006
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
TCH006_2.py:4:4: TCH006 Invalid string member in `X | Y`-style union type
|
||||
|
|
||||
4 | x: "int" | str # TCH006
|
||||
| ^^^^^ TCH006
|
||||
5 | x: ("int" | str) | "bool" # TCH006
|
||||
|
|
||||
|
||||
TCH006_2.py:5:5: TCH006 Invalid string member in `X | Y`-style union type
|
||||
|
|
||||
4 | x: "int" | str # TCH006
|
||||
5 | x: ("int" | str) | "bool" # TCH006
|
||||
| ^^^^^ TCH006
|
||||
|
|
||||
|
||||
TCH006_2.py:5:20: TCH006 Invalid string member in `X | Y`-style union type
|
||||
|
|
||||
4 | x: "int" | str # TCH006
|
||||
5 | x: ("int" | str) | "bool" # TCH006
|
||||
| ^^^^^^ TCH006
|
||||
|
|
||||
|
||||
TCH006_2.py:12:20: TCH006 Invalid string member in `X | Y`-style union type
|
||||
|
|
||||
12 | z: list[str, str | "int"] = [] # TCH006
|
||||
| ^^^^^ TCH006
|
||||
13 |
|
||||
14 | type A = Value["int" | str] # OK
|
||||
|
|
||||
|
||||
TCH006_2.py:16:30: TCH006 Invalid string member in `X | Y`-style union type
|
||||
|
|
||||
14 | type A = Value["int" | str] # OK
|
||||
15 |
|
||||
16 | OldS = TypeVar('OldS', int | 'str', str) # TCH006
|
||||
| ^^^^^ TCH006
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
exempt_type_checking_1.py:5:20: TCH003 [*] Move standard library import `typing.Final` into a type-checking block
|
||||
|
|
||||
3 | from __future__ import annotations
|
||||
4 |
|
||||
5 | from typing import Final
|
||||
| ^^^^^ TCH003
|
||||
6 |
|
||||
7 | Const: Final[dict] = {}
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 |
|
||||
3 3 | from __future__ import annotations
|
||||
4 4 |
|
||||
5 |-from typing import Final
|
||||
5 |+from typing import TYPE_CHECKING
|
||||
6 |+
|
||||
7 |+if TYPE_CHECKING:
|
||||
8 |+ from typing import Final
|
||||
6 9 |
|
||||
7 10 | Const: Final[dict] = {}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
exempt_type_checking_2.py:5:20: TCH003 [*] Move standard library import `typing.Final` into a type-checking block
|
||||
|
|
||||
3 | from __future__ import annotations
|
||||
4 |
|
||||
5 | from typing import Final, TYPE_CHECKING
|
||||
| ^^^^^ TCH003
|
||||
6 |
|
||||
7 | Const: Final[dict] = {}
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 |
|
||||
3 3 | from __future__ import annotations
|
||||
4 4 |
|
||||
5 |-from typing import Final, TYPE_CHECKING
|
||||
5 |+from typing import TYPE_CHECKING
|
||||
6 |+
|
||||
7 |+if TYPE_CHECKING:
|
||||
8 |+ from typing import Final
|
||||
6 9 |
|
||||
7 10 | Const: Final[dict] = {}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
exempt_type_checking_3.py:5:20: TCH003 [*] Move standard library import `typing.Final` into a type-checking block
|
||||
|
|
||||
3 | from __future__ import annotations
|
||||
4 |
|
||||
5 | from typing import Final, Mapping
|
||||
| ^^^^^ TCH003
|
||||
6 |
|
||||
7 | Const: Final[dict] = {}
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 |
|
||||
3 3 | from __future__ import annotations
|
||||
4 4 |
|
||||
5 |-from typing import Final, Mapping
|
||||
5 |+from typing import Mapping
|
||||
6 |+from typing import TYPE_CHECKING
|
||||
7 |+
|
||||
8 |+if TYPE_CHECKING:
|
||||
9 |+ from typing import Final
|
||||
6 10 |
|
||||
7 11 | Const: Final[dict] = {}
|
||||
|
||||
|
||||
@@ -51,6 +51,19 @@ use crate::settings::LinterSettings;
|
||||
/// """
|
||||
/// ```
|
||||
///
|
||||
/// ## Error suppression
|
||||
/// Hint: when suppressing `W505` errors within multi-line strings (like
|
||||
/// docstrings), the `noqa` directive should come at the end of the string
|
||||
/// (after the closing triple quote), and will apply to the entire string, like
|
||||
/// so:
|
||||
///
|
||||
/// ```python
|
||||
/// """Lorem ipsum dolor sit amet.
|
||||
///
|
||||
/// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
|
||||
/// """ # noqa: W505
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `task-tags`
|
||||
/// - `pycodestyle.max-doc-length`
|
||||
|
||||
@@ -45,11 +45,24 @@ use crate::settings::LinterSettings;
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## Error suppression
|
||||
/// Hint: when suppressing `E501` errors within multi-line strings (like
|
||||
/// docstrings), the `noqa` directive should come at the end of the string
|
||||
/// (after the closing triple quote), and will apply to the entire string, like
|
||||
/// so:
|
||||
///
|
||||
/// ```python
|
||||
/// """Lorem ipsum dolor sit amet.
|
||||
///
|
||||
/// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
|
||||
/// """ # noqa: E501
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `line-length`
|
||||
/// - `pycodestyle.max-line-length`
|
||||
/// - `task-tags`
|
||||
/// - `pycodestyle.ignore-overlong-task-comments`
|
||||
/// - `pycodestyle.max-line-length`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length
|
||||
#[violation]
|
||||
|
||||
@@ -254,14 +254,26 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
|
||||
Edit::range_deletion(TextRange::at(line.start(), line_indent.text_len()))
|
||||
} else {
|
||||
// Convert the character count to an offset within the source.
|
||||
// Example, where `[]` is a 2 byte non-breaking space:
|
||||
// ```
|
||||
// def f():
|
||||
// """ Docstring header
|
||||
// ^^^^ Real indentation is 4 chars
|
||||
// docstring body, over-indented
|
||||
// ^^^^^^ Over-indentation is 6 - 4 = 2 chars due to this line
|
||||
// [] [] docstring body 2, further indented
|
||||
// ^^^^^ We take these 4 chars/5 bytes to match the docstring ...
|
||||
// ^^^ ... and these 2 chars/3 bytes to remove the `over_indented_size` ...
|
||||
// ^^ ... but preserve this real indent
|
||||
// ```
|
||||
let offset = checker
|
||||
.locator()
|
||||
.after(line.start() + indent.text_len())
|
||||
.after(line.start())
|
||||
.chars()
|
||||
.take(over_indented_size)
|
||||
.take(docstring.indentation.chars().count() + over_indented_size)
|
||||
.map(TextLen::text_len)
|
||||
.sum::<TextSize>();
|
||||
let range = TextRange::at(line.start(), indent.text_len() + offset);
|
||||
let range = TextRange::at(line.start(), offset);
|
||||
Edit::range_replacement(indent, range)
|
||||
};
|
||||
diagnostic.set_fix(Fix::safe_edit(edit));
|
||||
|
||||
@@ -411,4 +411,22 @@ D.py:707:1: D208 [*] Docstring is over-indented
|
||||
709 709 |
|
||||
710 710 |
|
||||
|
||||
D.py:723:1: D208 [*] Docstring is over-indented
|
||||
|
|
||||
721 | """There's a non-breaking space (2-bytes) after 3 spaces (https://github.com/astral-sh/ruff/issues/9080).
|
||||
722 |
|
||||
723 | Returns:
|
||||
| D208
|
||||
724 | """
|
||||
|
|
||||
= help: Remove over-indentation
|
||||
|
||||
ℹ Safe fix
|
||||
720 720 | def inconsistent_indent_byte_size():
|
||||
721 721 | """There's a non-breaking space (2-bytes) after 3 spaces (https://github.com/astral-sh/ruff/issues/9080).
|
||||
722 722 |
|
||||
723 |- Returns:
|
||||
723 |+ Returns:
|
||||
724 724 | """
|
||||
|
||||
|
||||
|
||||
@@ -662,7 +662,7 @@ D.py:712:5: D213 [*] Multi-line docstring summary should start at the second lin
|
||||
713 | |
|
||||
714 | | This is not overindented
|
||||
715 | | This is overindented, but since one line is not overindented this should not raise
|
||||
716 | | And so is this, but it we should preserve the extra space on this line relative
|
||||
716 | | And so is this, but it we should preserve the extra space on this line relative
|
||||
717 | | """
|
||||
| |_______^ D213
|
||||
|
|
||||
@@ -679,4 +679,27 @@ D.py:712:5: D213 [*] Multi-line docstring summary should start at the second lin
|
||||
714 715 | This is not overindented
|
||||
715 716 | This is overindented, but since one line is not overindented this should not raise
|
||||
|
||||
D.py:721:5: D213 [*] Multi-line docstring summary should start at the second line
|
||||
|
|
||||
720 | def inconsistent_indent_byte_size():
|
||||
721 | """There's a non-breaking space (2-bytes) after 3 spaces (https://github.com/astral-sh/ruff/issues/9080).
|
||||
| _____^
|
||||
722 | |
|
||||
723 | | Returns:
|
||||
724 | | """
|
||||
| |_______^ D213
|
||||
|
|
||||
= help: Insert line break and indentation after opening quotes
|
||||
|
||||
ℹ Safe fix
|
||||
718 718 |
|
||||
719 719 |
|
||||
720 720 | def inconsistent_indent_byte_size():
|
||||
721 |- """There's a non-breaking space (2-bytes) after 3 spaces (https://github.com/astral-sh/ruff/issues/9080).
|
||||
721 |+ """
|
||||
722 |+ There's a non-breaking space (2-bytes) after 3 spaces (https://github.com/astral-sh/ruff/issues/9080).
|
||||
722 723 |
|
||||
723 724 | Returns:
|
||||
724 725 | """
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ mod tests {
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_17.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_18.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_19.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_20.py"))]
|
||||
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
|
||||
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
|
||||
#[test_case(Rule::LateFutureImport, Path::new("F404.py"))]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
@@ -346,6 +346,22 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_many_locals() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pylint/too_many_locals.py"),
|
||||
&LinterSettings {
|
||||
pylint: pylint::settings::Settings {
|
||||
max_locals: 15,
|
||||
..pylint::settings::Settings::default()
|
||||
},
|
||||
..LinterSettings::for_rules(vec![Rule::TooManyLocals])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unspecified_encoding_python39_or_lower() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -55,6 +55,7 @@ pub(crate) use sys_exit_alias::*;
|
||||
pub(crate) use too_many_arguments::*;
|
||||
pub(crate) use too_many_boolean_expressions::*;
|
||||
pub(crate) use too_many_branches::*;
|
||||
pub(crate) use too_many_locals::*;
|
||||
pub(crate) use too_many_positional::*;
|
||||
pub(crate) use too_many_public_methods::*;
|
||||
pub(crate) use too_many_return_statements::*;
|
||||
@@ -132,6 +133,7 @@ mod sys_exit_alias;
|
||||
mod too_many_arguments;
|
||||
mod too_many_boolean_expressions;
|
||||
mod too_many_branches;
|
||||
mod too_many_locals;
|
||||
mod too_many_positional;
|
||||
mod too_many_public_methods;
|
||||
mod too_many_return_statements;
|
||||
|
||||
59
crates/ruff_linter/src/rules/pylint/rules/too_many_locals.rs
Normal file
59
crates/ruff_linter/src/rules/pylint/rules/too_many_locals.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_semantic::{Scope, ScopeKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for functions that include too many local variables.
|
||||
///
|
||||
/// By default, this rule allows up to fifteen locals, as configured by the
|
||||
/// [`pylint.max-locals`] option.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Functions with many local variables are harder to understand and maintain.
|
||||
///
|
||||
/// Consider refactoring functions with many local variables into smaller
|
||||
/// functions with fewer assignments.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `pylint.max-locals`
|
||||
#[violation]
|
||||
pub struct TooManyLocals {
|
||||
current_amount: usize,
|
||||
max_amount: usize,
|
||||
}
|
||||
|
||||
impl Violation for TooManyLocals {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let TooManyLocals {
|
||||
current_amount,
|
||||
max_amount,
|
||||
} = self;
|
||||
format!("Too many local variables: ({current_amount}/{max_amount})")
|
||||
}
|
||||
}
|
||||
|
||||
/// PLR0914
|
||||
pub(crate) fn too_many_locals(checker: &Checker, scope: &Scope, diagnostics: &mut Vec<Diagnostic>) {
|
||||
let num_locals = scope
|
||||
.binding_ids()
|
||||
.filter(|id| {
|
||||
let binding = checker.semantic().binding(*id);
|
||||
binding.kind.is_assignment()
|
||||
})
|
||||
.count();
|
||||
if num_locals > checker.settings.pylint.max_locals {
|
||||
if let ScopeKind::Function(func) = scope.kind {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
TooManyLocals {
|
||||
current_amount: num_locals,
|
||||
max_amount: checker.settings.pylint.max_locals,
|
||||
},
|
||||
func.identifier(),
|
||||
));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ pub struct Settings {
|
||||
pub max_branches: usize,
|
||||
pub max_statements: usize,
|
||||
pub max_public_methods: usize,
|
||||
pub max_locals: usize,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
@@ -59,6 +60,7 @@ impl Default for Settings {
|
||||
max_branches: 12,
|
||||
max_statements: 50,
|
||||
max_public_methods: 20,
|
||||
max_locals: 15,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
too_many_locals.py:20:5: PLR0914 Too many local variables: (16/15)
|
||||
|
|
||||
20 | def func() -> None: # PLR0914
|
||||
| ^^^^ PLR0914
|
||||
21 | first = 1
|
||||
22 | second = 2
|
||||
|
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::ReadWholeFile, Path::new("FURB101.py"))]
|
||||
#[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))]
|
||||
#[test_case(Rule::ReimplementedOperator, Path::new("FURB118.py"))]
|
||||
#[test_case(Rule::DeleteFullSlice, Path::new("FURB131.py"))]
|
||||
#[test_case(Rule::CheckAndRemoveFromSet, Path::new("FURB132.py"))]
|
||||
#[test_case(Rule::IfExprMinMax, Path::new("FURB136.py"))]
|
||||
|
||||
@@ -8,6 +8,7 @@ pub(crate) use math_constant::*;
|
||||
pub(crate) use print_empty_string::*;
|
||||
pub(crate) use read_whole_file::*;
|
||||
pub(crate) use redundant_log_base::*;
|
||||
pub(crate) use reimplemented_operator::*;
|
||||
pub(crate) use reimplemented_starmap::*;
|
||||
pub(crate) use repeated_append::*;
|
||||
pub(crate) use single_item_membership_test::*;
|
||||
@@ -25,6 +26,7 @@ mod math_constant;
|
||||
mod print_empty_string;
|
||||
mod read_whole_file;
|
||||
mod redundant_log_base;
|
||||
mod reimplemented_operator;
|
||||
mod reimplemented_starmap;
|
||||
mod repeated_append;
|
||||
mod single_item_membership_test;
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
use anyhow::{bail, Result};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::{ImportRequest, Importer};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for lambda expressions and function definitions that can be replaced
|
||||
/// with a function from the `operator` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `operator` module provides functions that implement the same functionality
|
||||
/// as the corresponding operators. For example, `operator.add` is equivalent to
|
||||
/// `lambda x, y: x + y`. Using the functions from the `operator` module is more
|
||||
/// concise and communicates the intent of the code more clearly.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import functools
|
||||
///
|
||||
/// nums = [1, 2, 3]
|
||||
/// sum = functools.reduce(lambda x, y: x + y, nums)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import functools
|
||||
/// import operator
|
||||
///
|
||||
/// nums = [1, 2, 3]
|
||||
/// sum = functools.reduce(operator.add, nums)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
#[violation]
|
||||
pub struct ReimplementedOperator {
|
||||
target: &'static str,
|
||||
operator: &'static str,
|
||||
}
|
||||
|
||||
impl Violation for ReimplementedOperator {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let ReimplementedOperator { operator, target } = self;
|
||||
format!("Use `operator.{operator}` instead of defining a {target}")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let ReimplementedOperator { operator, .. } = self;
|
||||
Some(format!("Replace with `operator.{operator}`"))
|
||||
}
|
||||
}
|
||||
|
||||
/// FURB118
|
||||
pub(crate) fn reimplemented_operator(checker: &mut Checker, target: &FunctionLike) {
|
||||
let Some(params) = target.parameters() else {
|
||||
return;
|
||||
};
|
||||
let Some(body) = target.body() else { return };
|
||||
let Some(operator) = get_operator(body, params) else {
|
||||
return;
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
ReimplementedOperator {
|
||||
operator,
|
||||
target: target.kind(),
|
||||
},
|
||||
target.range(),
|
||||
);
|
||||
diagnostic.try_set_fix(|| target.try_fix(operator, checker.importer(), checker.semantic()));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Candidate for lambda expression or function definition consisting of a return statement.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum FunctionLike<'a> {
|
||||
Lambda(&'a ast::ExprLambda),
|
||||
Function(&'a ast::StmtFunctionDef),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprLambda> for FunctionLike<'a> {
|
||||
fn from(lambda: &'a ast::ExprLambda) -> Self {
|
||||
Self::Lambda(lambda)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtFunctionDef> for FunctionLike<'a> {
|
||||
fn from(function: &'a ast::StmtFunctionDef) -> Self {
|
||||
Self::Function(function)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for FunctionLike<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
Self::Lambda(expr) => expr.range(),
|
||||
Self::Function(stmt) => stmt.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionLike<'_> {
|
||||
/// Return the [`ast::Parameters`] of the function-like node.
|
||||
fn parameters(&self) -> Option<&ast::Parameters> {
|
||||
match self {
|
||||
Self::Lambda(expr) => expr.parameters.as_deref(),
|
||||
Self::Function(stmt) => Some(&stmt.parameters),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the body of the function-like node.
|
||||
///
|
||||
/// If the node is a function definition that consists of more than a single return statement,
|
||||
/// returns `None`.
|
||||
fn body(&self) -> Option<&Expr> {
|
||||
match self {
|
||||
Self::Lambda(expr) => Some(&expr.body),
|
||||
Self::Function(stmt) => match stmt.body.as_slice() {
|
||||
[Stmt::Return(ast::StmtReturn { value, .. })] => value.as_deref(),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the display kind of the function-like node.
|
||||
fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Lambda(_) => "lambda",
|
||||
Self::Function(_) => "function",
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to fix the function-like node by replacing it with a call to the corresponding
|
||||
/// function from `operator` module.
|
||||
fn try_fix(
|
||||
&self,
|
||||
operator: &'static str,
|
||||
importer: &Importer,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<Fix> {
|
||||
match self {
|
||||
Self::Lambda(_) => {
|
||||
let (edit, binding) = importer.get_or_import_symbol(
|
||||
&ImportRequest::import("operator", operator),
|
||||
self.start(),
|
||||
semantic,
|
||||
)?;
|
||||
Ok(Fix::safe_edits(
|
||||
Edit::range_replacement(binding, self.range()),
|
||||
[edit],
|
||||
))
|
||||
}
|
||||
Self::Function(_) => bail!("No fix available"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the name of the `operator` implemented by the given expression.
|
||||
fn get_operator(expr: &Expr, params: &ast::Parameters) -> Option<&'static str> {
|
||||
match expr {
|
||||
Expr::UnaryOp(expr) => unary_op(expr, params),
|
||||
Expr::BinOp(expr) => bin_op(expr, params),
|
||||
Expr::Compare(expr) => cmp_op(expr, params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the name of the `operator` implemented by the given unary expression.
|
||||
fn unary_op(expr: &ast::ExprUnaryOp, params: &ast::Parameters) -> Option<&'static str> {
|
||||
let [arg] = params.args.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
if !is_same_expression(arg, &expr.operand) {
|
||||
return None;
|
||||
}
|
||||
Some(match expr.op {
|
||||
ast::UnaryOp::Invert => "invert",
|
||||
ast::UnaryOp::Not => "not_",
|
||||
ast::UnaryOp::UAdd => "pos",
|
||||
ast::UnaryOp::USub => "neg",
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the name of the `operator` implemented by the given binary expression.
|
||||
fn bin_op(expr: &ast::ExprBinOp, params: &ast::Parameters) -> Option<&'static str> {
|
||||
let [arg1, arg2] = params.args.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
if !is_same_expression(arg1, &expr.left) || !is_same_expression(arg2, &expr.right) {
|
||||
return None;
|
||||
}
|
||||
Some(match expr.op {
|
||||
ast::Operator::Add => "add",
|
||||
ast::Operator::Sub => "sub",
|
||||
ast::Operator::Mult => "mul",
|
||||
ast::Operator::MatMult => "matmul",
|
||||
ast::Operator::Div => "truediv",
|
||||
ast::Operator::Mod => "mod",
|
||||
ast::Operator::Pow => "pow",
|
||||
ast::Operator::LShift => "lshift",
|
||||
ast::Operator::RShift => "rshift",
|
||||
ast::Operator::BitOr => "or_",
|
||||
ast::Operator::BitXor => "xor",
|
||||
ast::Operator::BitAnd => "and_",
|
||||
ast::Operator::FloorDiv => "floordiv",
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the name of the `operator` implemented by the given comparison expression.
|
||||
fn cmp_op(expr: &ast::ExprCompare, params: &ast::Parameters) -> Option<&'static str> {
|
||||
let [arg1, arg2] = params.args.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
let [op] = expr.ops.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
let [right] = expr.comparators.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
match op {
|
||||
ast::CmpOp::Eq => {
|
||||
if match_arguments(arg1, arg2, &expr.left, right) {
|
||||
Some("eq")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ast::CmpOp::NotEq => {
|
||||
if match_arguments(arg1, arg2, &expr.left, right) {
|
||||
Some("ne")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ast::CmpOp::Lt => {
|
||||
if match_arguments(arg1, arg2, &expr.left, right) {
|
||||
Some("lt")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ast::CmpOp::LtE => {
|
||||
if match_arguments(arg1, arg2, &expr.left, right) {
|
||||
Some("le")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ast::CmpOp::Gt => {
|
||||
if match_arguments(arg1, arg2, &expr.left, right) {
|
||||
Some("gt")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ast::CmpOp::GtE => {
|
||||
if match_arguments(arg1, arg2, &expr.left, right) {
|
||||
Some("ge")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ast::CmpOp::Is => {
|
||||
if match_arguments(arg1, arg2, &expr.left, right) {
|
||||
Some("is_")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ast::CmpOp::IsNot => {
|
||||
if match_arguments(arg1, arg2, &expr.left, right) {
|
||||
Some("is_not")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ast::CmpOp::In => {
|
||||
// Note: `operator.contains` reverses the order of arguments. That is:
|
||||
// `operator.contains` is equivalent to `lambda x, y: y in x`, rather than
|
||||
// `lambda x, y: x in y`.
|
||||
if match_arguments(arg1, arg2, right, &expr.left) {
|
||||
Some("contains")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ast::CmpOp::NotIn => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given arguments match the expected operands.
|
||||
fn match_arguments(
|
||||
arg1: &ast::ParameterWithDefault,
|
||||
arg2: &ast::ParameterWithDefault,
|
||||
operand1: &Expr,
|
||||
operand2: &Expr,
|
||||
) -> bool {
|
||||
is_same_expression(arg1, operand1) && is_same_expression(arg2, operand2)
|
||||
}
|
||||
|
||||
/// Returns `true` if the given argument is the "same" as the given expression. For example, if
|
||||
/// the argument has a default, it is not considered the same as any expression; if both match the
|
||||
/// same name, they are considered the same.
|
||||
fn is_same_expression(arg: &ast::ParameterWithDefault, expr: &Expr) -> bool {
|
||||
if arg.default.is_some() {
|
||||
false
|
||||
} else if let Expr::Name(name) = expr {
|
||||
name.id == arg.parameter.name.as_str()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB118.py:2:13: FURB118 [*] Use `operator.invert` instead of defining a lambda
|
||||
|
|
||||
1 | # Errors.
|
||||
2 | op_bitnot = lambda x: ~x
|
||||
| ^^^^^^^^^^^^ FURB118
|
||||
3 | op_not = lambda x: not x
|
||||
4 | op_pos = lambda x: +x
|
||||
|
|
||||
= help: Replace with `operator.invert`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |-op_bitnot = lambda x: ~x
|
||||
2 |+import operator
|
||||
3 |+op_bitnot = operator.invert
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
5 6 | op_neg = lambda x: -x
|
||||
|
||||
FURB118.py:3:10: FURB118 [*] Use `operator.not_` instead of defining a lambda
|
||||
|
|
||||
1 | # Errors.
|
||||
2 | op_bitnot = lambda x: ~x
|
||||
3 | op_not = lambda x: not x
|
||||
| ^^^^^^^^^^^^^^^ FURB118
|
||||
4 | op_pos = lambda x: +x
|
||||
5 | op_neg = lambda x: -x
|
||||
|
|
||||
= help: Replace with `operator.not_`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 |-op_not = lambda x: not x
|
||||
4 |+op_not = operator.not_
|
||||
4 5 | op_pos = lambda x: +x
|
||||
5 6 | op_neg = lambda x: -x
|
||||
6 7 |
|
||||
|
||||
FURB118.py:4:10: FURB118 [*] Use `operator.pos` instead of defining a lambda
|
||||
|
|
||||
2 | op_bitnot = lambda x: ~x
|
||||
3 | op_not = lambda x: not x
|
||||
4 | op_pos = lambda x: +x
|
||||
| ^^^^^^^^^^^^ FURB118
|
||||
5 | op_neg = lambda x: -x
|
||||
|
|
||||
= help: Replace with `operator.pos`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 |-op_pos = lambda x: +x
|
||||
5 |+op_pos = operator.pos
|
||||
5 6 | op_neg = lambda x: -x
|
||||
6 7 |
|
||||
7 8 | op_add = lambda x, y: x + y
|
||||
|
||||
FURB118.py:5:10: FURB118 [*] Use `operator.neg` instead of defining a lambda
|
||||
|
|
||||
3 | op_not = lambda x: not x
|
||||
4 | op_pos = lambda x: +x
|
||||
5 | op_neg = lambda x: -x
|
||||
| ^^^^^^^^^^^^ FURB118
|
||||
6 |
|
||||
7 | op_add = lambda x, y: x + y
|
||||
|
|
||||
= help: Replace with `operator.neg`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
5 |-op_neg = lambda x: -x
|
||||
6 |+op_neg = operator.neg
|
||||
6 7 |
|
||||
7 8 | op_add = lambda x, y: x + y
|
||||
8 9 | op_sub = lambda x, y: x - y
|
||||
|
||||
FURB118.py:7:10: FURB118 [*] Use `operator.add` instead of defining a lambda
|
||||
|
|
||||
5 | op_neg = lambda x: -x
|
||||
6 |
|
||||
7 | op_add = lambda x, y: x + y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
8 | op_sub = lambda x, y: x - y
|
||||
9 | op_mult = lambda x, y: x * y
|
||||
|
|
||||
= help: Replace with `operator.add`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
5 6 | op_neg = lambda x: -x
|
||||
6 7 |
|
||||
7 |-op_add = lambda x, y: x + y
|
||||
8 |+op_add = operator.add
|
||||
8 9 | op_sub = lambda x, y: x - y
|
||||
9 10 | op_mult = lambda x, y: x * y
|
||||
10 11 | op_matmutl = lambda x, y: x @ y
|
||||
|
||||
FURB118.py:8:10: FURB118 [*] Use `operator.sub` instead of defining a lambda
|
||||
|
|
||||
7 | op_add = lambda x, y: x + y
|
||||
8 | op_sub = lambda x, y: x - y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
9 | op_mult = lambda x, y: x * y
|
||||
10 | op_matmutl = lambda x, y: x @ y
|
||||
|
|
||||
= help: Replace with `operator.sub`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
5 6 | op_neg = lambda x: -x
|
||||
6 7 |
|
||||
7 8 | op_add = lambda x, y: x + y
|
||||
8 |-op_sub = lambda x, y: x - y
|
||||
9 |+op_sub = operator.sub
|
||||
9 10 | op_mult = lambda x, y: x * y
|
||||
10 11 | op_matmutl = lambda x, y: x @ y
|
||||
11 12 | op_truediv = lambda x, y: x / y
|
||||
|
||||
FURB118.py:9:11: FURB118 [*] Use `operator.mul` instead of defining a lambda
|
||||
|
|
||||
7 | op_add = lambda x, y: x + y
|
||||
8 | op_sub = lambda x, y: x - y
|
||||
9 | op_mult = lambda x, y: x * y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
10 | op_matmutl = lambda x, y: x @ y
|
||||
11 | op_truediv = lambda x, y: x / y
|
||||
|
|
||||
= help: Replace with `operator.mul`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
6 7 |
|
||||
7 8 | op_add = lambda x, y: x + y
|
||||
8 9 | op_sub = lambda x, y: x - y
|
||||
9 |-op_mult = lambda x, y: x * y
|
||||
10 |+op_mult = operator.mul
|
||||
10 11 | op_matmutl = lambda x, y: x @ y
|
||||
11 12 | op_truediv = lambda x, y: x / y
|
||||
12 13 | op_mod = lambda x, y: x % y
|
||||
|
||||
FURB118.py:10:14: FURB118 [*] Use `operator.matmul` instead of defining a lambda
|
||||
|
|
||||
8 | op_sub = lambda x, y: x - y
|
||||
9 | op_mult = lambda x, y: x * y
|
||||
10 | op_matmutl = lambda x, y: x @ y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
11 | op_truediv = lambda x, y: x / y
|
||||
12 | op_mod = lambda x, y: x % y
|
||||
|
|
||||
= help: Replace with `operator.matmul`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
7 8 | op_add = lambda x, y: x + y
|
||||
8 9 | op_sub = lambda x, y: x - y
|
||||
9 10 | op_mult = lambda x, y: x * y
|
||||
10 |-op_matmutl = lambda x, y: x @ y
|
||||
11 |+op_matmutl = operator.matmul
|
||||
11 12 | op_truediv = lambda x, y: x / y
|
||||
12 13 | op_mod = lambda x, y: x % y
|
||||
13 14 | op_pow = lambda x, y: x ** y
|
||||
|
||||
FURB118.py:11:14: FURB118 [*] Use `operator.truediv` instead of defining a lambda
|
||||
|
|
||||
9 | op_mult = lambda x, y: x * y
|
||||
10 | op_matmutl = lambda x, y: x @ y
|
||||
11 | op_truediv = lambda x, y: x / y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
12 | op_mod = lambda x, y: x % y
|
||||
13 | op_pow = lambda x, y: x ** y
|
||||
|
|
||||
= help: Replace with `operator.truediv`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
8 9 | op_sub = lambda x, y: x - y
|
||||
9 10 | op_mult = lambda x, y: x * y
|
||||
10 11 | op_matmutl = lambda x, y: x @ y
|
||||
11 |-op_truediv = lambda x, y: x / y
|
||||
12 |+op_truediv = operator.truediv
|
||||
12 13 | op_mod = lambda x, y: x % y
|
||||
13 14 | op_pow = lambda x, y: x ** y
|
||||
14 15 | op_lshift = lambda x, y: x << y
|
||||
|
||||
FURB118.py:12:10: FURB118 [*] Use `operator.mod` instead of defining a lambda
|
||||
|
|
||||
10 | op_matmutl = lambda x, y: x @ y
|
||||
11 | op_truediv = lambda x, y: x / y
|
||||
12 | op_mod = lambda x, y: x % y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
13 | op_pow = lambda x, y: x ** y
|
||||
14 | op_lshift = lambda x, y: x << y
|
||||
|
|
||||
= help: Replace with `operator.mod`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
9 10 | op_mult = lambda x, y: x * y
|
||||
10 11 | op_matmutl = lambda x, y: x @ y
|
||||
11 12 | op_truediv = lambda x, y: x / y
|
||||
12 |-op_mod = lambda x, y: x % y
|
||||
13 |+op_mod = operator.mod
|
||||
13 14 | op_pow = lambda x, y: x ** y
|
||||
14 15 | op_lshift = lambda x, y: x << y
|
||||
15 16 | op_rshift = lambda x, y: x >> y
|
||||
|
||||
FURB118.py:13:10: FURB118 [*] Use `operator.pow` instead of defining a lambda
|
||||
|
|
||||
11 | op_truediv = lambda x, y: x / y
|
||||
12 | op_mod = lambda x, y: x % y
|
||||
13 | op_pow = lambda x, y: x ** y
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
14 | op_lshift = lambda x, y: x << y
|
||||
15 | op_rshift = lambda x, y: x >> y
|
||||
|
|
||||
= help: Replace with `operator.pow`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
10 11 | op_matmutl = lambda x, y: x @ y
|
||||
11 12 | op_truediv = lambda x, y: x / y
|
||||
12 13 | op_mod = lambda x, y: x % y
|
||||
13 |-op_pow = lambda x, y: x ** y
|
||||
14 |+op_pow = operator.pow
|
||||
14 15 | op_lshift = lambda x, y: x << y
|
||||
15 16 | op_rshift = lambda x, y: x >> y
|
||||
16 17 | op_bitor = lambda x, y: x | y
|
||||
|
||||
FURB118.py:14:13: FURB118 [*] Use `operator.lshift` instead of defining a lambda
|
||||
|
|
||||
12 | op_mod = lambda x, y: x % y
|
||||
13 | op_pow = lambda x, y: x ** y
|
||||
14 | op_lshift = lambda x, y: x << y
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
15 | op_rshift = lambda x, y: x >> y
|
||||
16 | op_bitor = lambda x, y: x | y
|
||||
|
|
||||
= help: Replace with `operator.lshift`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
11 12 | op_truediv = lambda x, y: x / y
|
||||
12 13 | op_mod = lambda x, y: x % y
|
||||
13 14 | op_pow = lambda x, y: x ** y
|
||||
14 |-op_lshift = lambda x, y: x << y
|
||||
15 |+op_lshift = operator.lshift
|
||||
15 16 | op_rshift = lambda x, y: x >> y
|
||||
16 17 | op_bitor = lambda x, y: x | y
|
||||
17 18 | op_xor = lambda x, y: x ^ y
|
||||
|
||||
FURB118.py:15:13: FURB118 [*] Use `operator.rshift` instead of defining a lambda
|
||||
|
|
||||
13 | op_pow = lambda x, y: x ** y
|
||||
14 | op_lshift = lambda x, y: x << y
|
||||
15 | op_rshift = lambda x, y: x >> y
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
16 | op_bitor = lambda x, y: x | y
|
||||
17 | op_xor = lambda x, y: x ^ y
|
||||
|
|
||||
= help: Replace with `operator.rshift`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
12 13 | op_mod = lambda x, y: x % y
|
||||
13 14 | op_pow = lambda x, y: x ** y
|
||||
14 15 | op_lshift = lambda x, y: x << y
|
||||
15 |-op_rshift = lambda x, y: x >> y
|
||||
16 |+op_rshift = operator.rshift
|
||||
16 17 | op_bitor = lambda x, y: x | y
|
||||
17 18 | op_xor = lambda x, y: x ^ y
|
||||
18 19 | op_bitand = lambda x, y: x & y
|
||||
|
||||
FURB118.py:16:12: FURB118 [*] Use `operator.or_` instead of defining a lambda
|
||||
|
|
||||
14 | op_lshift = lambda x, y: x << y
|
||||
15 | op_rshift = lambda x, y: x >> y
|
||||
16 | op_bitor = lambda x, y: x | y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
17 | op_xor = lambda x, y: x ^ y
|
||||
18 | op_bitand = lambda x, y: x & y
|
||||
|
|
||||
= help: Replace with `operator.or_`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
13 14 | op_pow = lambda x, y: x ** y
|
||||
14 15 | op_lshift = lambda x, y: x << y
|
||||
15 16 | op_rshift = lambda x, y: x >> y
|
||||
16 |-op_bitor = lambda x, y: x | y
|
||||
17 |+op_bitor = operator.or_
|
||||
17 18 | op_xor = lambda x, y: x ^ y
|
||||
18 19 | op_bitand = lambda x, y: x & y
|
||||
19 20 | op_floordiv = lambda x, y: x // y
|
||||
|
||||
FURB118.py:17:10: FURB118 [*] Use `operator.xor` instead of defining a lambda
|
||||
|
|
||||
15 | op_rshift = lambda x, y: x >> y
|
||||
16 | op_bitor = lambda x, y: x | y
|
||||
17 | op_xor = lambda x, y: x ^ y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
18 | op_bitand = lambda x, y: x & y
|
||||
19 | op_floordiv = lambda x, y: x // y
|
||||
|
|
||||
= help: Replace with `operator.xor`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
14 15 | op_lshift = lambda x, y: x << y
|
||||
15 16 | op_rshift = lambda x, y: x >> y
|
||||
16 17 | op_bitor = lambda x, y: x | y
|
||||
17 |-op_xor = lambda x, y: x ^ y
|
||||
18 |+op_xor = operator.xor
|
||||
18 19 | op_bitand = lambda x, y: x & y
|
||||
19 20 | op_floordiv = lambda x, y: x // y
|
||||
20 21 |
|
||||
|
||||
FURB118.py:18:13: FURB118 [*] Use `operator.and_` instead of defining a lambda
|
||||
|
|
||||
16 | op_bitor = lambda x, y: x | y
|
||||
17 | op_xor = lambda x, y: x ^ y
|
||||
18 | op_bitand = lambda x, y: x & y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
19 | op_floordiv = lambda x, y: x // y
|
||||
|
|
||||
= help: Replace with `operator.and_`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
15 16 | op_rshift = lambda x, y: x >> y
|
||||
16 17 | op_bitor = lambda x, y: x | y
|
||||
17 18 | op_xor = lambda x, y: x ^ y
|
||||
18 |-op_bitand = lambda x, y: x & y
|
||||
19 |+op_bitand = operator.and_
|
||||
19 20 | op_floordiv = lambda x, y: x // y
|
||||
20 21 |
|
||||
21 22 | op_eq = lambda x, y: x == y
|
||||
|
||||
FURB118.py:19:15: FURB118 [*] Use `operator.floordiv` instead of defining a lambda
|
||||
|
|
||||
17 | op_xor = lambda x, y: x ^ y
|
||||
18 | op_bitand = lambda x, y: x & y
|
||||
19 | op_floordiv = lambda x, y: x // y
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
20 |
|
||||
21 | op_eq = lambda x, y: x == y
|
||||
|
|
||||
= help: Replace with `operator.floordiv`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
16 17 | op_bitor = lambda x, y: x | y
|
||||
17 18 | op_xor = lambda x, y: x ^ y
|
||||
18 19 | op_bitand = lambda x, y: x & y
|
||||
19 |-op_floordiv = lambda x, y: x // y
|
||||
20 |+op_floordiv = operator.floordiv
|
||||
20 21 |
|
||||
21 22 | op_eq = lambda x, y: x == y
|
||||
22 23 | op_ne = lambda x, y: x != y
|
||||
|
||||
FURB118.py:21:9: FURB118 [*] Use `operator.eq` instead of defining a lambda
|
||||
|
|
||||
19 | op_floordiv = lambda x, y: x // y
|
||||
20 |
|
||||
21 | op_eq = lambda x, y: x == y
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
22 | op_ne = lambda x, y: x != y
|
||||
23 | op_lt = lambda x, y: x < y
|
||||
|
|
||||
= help: Replace with `operator.eq`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
18 19 | op_bitand = lambda x, y: x & y
|
||||
19 20 | op_floordiv = lambda x, y: x // y
|
||||
20 21 |
|
||||
21 |-op_eq = lambda x, y: x == y
|
||||
22 |+op_eq = operator.eq
|
||||
22 23 | op_ne = lambda x, y: x != y
|
||||
23 24 | op_lt = lambda x, y: x < y
|
||||
24 25 | op_lte = lambda x, y: x <= y
|
||||
|
||||
FURB118.py:22:9: FURB118 [*] Use `operator.ne` instead of defining a lambda
|
||||
|
|
||||
21 | op_eq = lambda x, y: x == y
|
||||
22 | op_ne = lambda x, y: x != y
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
23 | op_lt = lambda x, y: x < y
|
||||
24 | op_lte = lambda x, y: x <= y
|
||||
|
|
||||
= help: Replace with `operator.ne`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
19 20 | op_floordiv = lambda x, y: x // y
|
||||
20 21 |
|
||||
21 22 | op_eq = lambda x, y: x == y
|
||||
22 |-op_ne = lambda x, y: x != y
|
||||
23 |+op_ne = operator.ne
|
||||
23 24 | op_lt = lambda x, y: x < y
|
||||
24 25 | op_lte = lambda x, y: x <= y
|
||||
25 26 | op_gt = lambda x, y: x > y
|
||||
|
||||
FURB118.py:23:9: FURB118 [*] Use `operator.lt` instead of defining a lambda
|
||||
|
|
||||
21 | op_eq = lambda x, y: x == y
|
||||
22 | op_ne = lambda x, y: x != y
|
||||
23 | op_lt = lambda x, y: x < y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
24 | op_lte = lambda x, y: x <= y
|
||||
25 | op_gt = lambda x, y: x > y
|
||||
|
|
||||
= help: Replace with `operator.lt`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
20 21 |
|
||||
21 22 | op_eq = lambda x, y: x == y
|
||||
22 23 | op_ne = lambda x, y: x != y
|
||||
23 |-op_lt = lambda x, y: x < y
|
||||
24 |+op_lt = operator.lt
|
||||
24 25 | op_lte = lambda x, y: x <= y
|
||||
25 26 | op_gt = lambda x, y: x > y
|
||||
26 27 | op_gte = lambda x, y: x >= y
|
||||
|
||||
FURB118.py:24:10: FURB118 [*] Use `operator.le` instead of defining a lambda
|
||||
|
|
||||
22 | op_ne = lambda x, y: x != y
|
||||
23 | op_lt = lambda x, y: x < y
|
||||
24 | op_lte = lambda x, y: x <= y
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
25 | op_gt = lambda x, y: x > y
|
||||
26 | op_gte = lambda x, y: x >= y
|
||||
|
|
||||
= help: Replace with `operator.le`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
21 22 | op_eq = lambda x, y: x == y
|
||||
22 23 | op_ne = lambda x, y: x != y
|
||||
23 24 | op_lt = lambda x, y: x < y
|
||||
24 |-op_lte = lambda x, y: x <= y
|
||||
25 |+op_lte = operator.le
|
||||
25 26 | op_gt = lambda x, y: x > y
|
||||
26 27 | op_gte = lambda x, y: x >= y
|
||||
27 28 | op_is = lambda x, y: x is y
|
||||
|
||||
FURB118.py:25:9: FURB118 [*] Use `operator.gt` instead of defining a lambda
|
||||
|
|
||||
23 | op_lt = lambda x, y: x < y
|
||||
24 | op_lte = lambda x, y: x <= y
|
||||
25 | op_gt = lambda x, y: x > y
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB118
|
||||
26 | op_gte = lambda x, y: x >= y
|
||||
27 | op_is = lambda x, y: x is y
|
||||
|
|
||||
= help: Replace with `operator.gt`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
22 23 | op_ne = lambda x, y: x != y
|
||||
23 24 | op_lt = lambda x, y: x < y
|
||||
24 25 | op_lte = lambda x, y: x <= y
|
||||
25 |-op_gt = lambda x, y: x > y
|
||||
26 |+op_gt = operator.gt
|
||||
26 27 | op_gte = lambda x, y: x >= y
|
||||
27 28 | op_is = lambda x, y: x is y
|
||||
28 29 | op_isnot = lambda x, y: x is not y
|
||||
|
||||
FURB118.py:26:10: FURB118 [*] Use `operator.ge` instead of defining a lambda
|
||||
|
|
||||
24 | op_lte = lambda x, y: x <= y
|
||||
25 | op_gt = lambda x, y: x > y
|
||||
26 | op_gte = lambda x, y: x >= y
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
27 | op_is = lambda x, y: x is y
|
||||
28 | op_isnot = lambda x, y: x is not y
|
||||
|
|
||||
= help: Replace with `operator.ge`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
23 24 | op_lt = lambda x, y: x < y
|
||||
24 25 | op_lte = lambda x, y: x <= y
|
||||
25 26 | op_gt = lambda x, y: x > y
|
||||
26 |-op_gte = lambda x, y: x >= y
|
||||
27 |+op_gte = operator.ge
|
||||
27 28 | op_is = lambda x, y: x is y
|
||||
28 29 | op_isnot = lambda x, y: x is not y
|
||||
29 30 | op_in = lambda x, y: y in x
|
||||
|
||||
FURB118.py:27:9: FURB118 [*] Use `operator.is_` instead of defining a lambda
|
||||
|
|
||||
25 | op_gt = lambda x, y: x > y
|
||||
26 | op_gte = lambda x, y: x >= y
|
||||
27 | op_is = lambda x, y: x is y
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
28 | op_isnot = lambda x, y: x is not y
|
||||
29 | op_in = lambda x, y: y in x
|
||||
|
|
||||
= help: Replace with `operator.is_`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
24 25 | op_lte = lambda x, y: x <= y
|
||||
25 26 | op_gt = lambda x, y: x > y
|
||||
26 27 | op_gte = lambda x, y: x >= y
|
||||
27 |-op_is = lambda x, y: x is y
|
||||
28 |+op_is = operator.is_
|
||||
28 29 | op_isnot = lambda x, y: x is not y
|
||||
29 30 | op_in = lambda x, y: y in x
|
||||
30 31 |
|
||||
|
||||
FURB118.py:28:12: FURB118 [*] Use `operator.is_not` instead of defining a lambda
|
||||
|
|
||||
26 | op_gte = lambda x, y: x >= y
|
||||
27 | op_is = lambda x, y: x is y
|
||||
28 | op_isnot = lambda x, y: x is not y
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
29 | op_in = lambda x, y: y in x
|
||||
|
|
||||
= help: Replace with `operator.is_not`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
25 26 | op_gt = lambda x, y: x > y
|
||||
26 27 | op_gte = lambda x, y: x >= y
|
||||
27 28 | op_is = lambda x, y: x is y
|
||||
28 |-op_isnot = lambda x, y: x is not y
|
||||
29 |+op_isnot = operator.is_not
|
||||
29 30 | op_in = lambda x, y: y in x
|
||||
30 31 |
|
||||
31 32 |
|
||||
|
||||
FURB118.py:29:9: FURB118 [*] Use `operator.contains` instead of defining a lambda
|
||||
|
|
||||
27 | op_is = lambda x, y: x is y
|
||||
28 | op_isnot = lambda x, y: x is not y
|
||||
29 | op_in = lambda x, y: y in x
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB118
|
||||
|
|
||||
= help: Replace with `operator.contains`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors.
|
||||
2 |+import operator
|
||||
2 3 | op_bitnot = lambda x: ~x
|
||||
3 4 | op_not = lambda x: not x
|
||||
4 5 | op_pos = lambda x: +x
|
||||
--------------------------------------------------------------------------------
|
||||
26 27 | op_gte = lambda x, y: x >= y
|
||||
27 28 | op_is = lambda x, y: x is y
|
||||
28 29 | op_isnot = lambda x, y: x is not y
|
||||
29 |-op_in = lambda x, y: y in x
|
||||
30 |+op_in = operator.contains
|
||||
30 31 |
|
||||
31 32 |
|
||||
32 33 | def op_not2(x):
|
||||
|
||||
FURB118.py:32:1: FURB118 Use `operator.not_` instead of defining a function
|
||||
|
|
||||
32 | / def op_not2(x):
|
||||
33 | | return not x
|
||||
| |________________^ FURB118
|
||||
|
|
||||
= help: Replace with `operator.not_`
|
||||
|
||||
FURB118.py:36:1: FURB118 Use `operator.add` instead of defining a function
|
||||
|
|
||||
36 | / def op_add2(x, y):
|
||||
37 | | return x + y
|
||||
| |________________^ FURB118
|
||||
|
|
||||
= help: Replace with `operator.add`
|
||||
|
||||
FURB118.py:41:5: FURB118 Use `operator.add` instead of defining a function
|
||||
|
|
||||
40 | class Adder:
|
||||
41 | def add(x, y):
|
||||
| _____^
|
||||
42 | | return x + y
|
||||
| |____________________^ FURB118
|
||||
43 |
|
||||
44 | # OK.
|
||||
|
|
||||
= help: Replace with `operator.add`
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use ast::Stmt;
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::{analyze::typing, Binding, SemanticModel};
|
||||
use ruff_python_semantic::{analyze::typing, Scope, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// ## What it does
|
||||
@@ -105,22 +105,50 @@ pub(crate) fn asyncio_dangling_task(expr: &Expr, semantic: &SemanticModel) -> Op
|
||||
|
||||
/// RUF006
|
||||
pub(crate) fn asyncio_dangling_binding(
|
||||
binding: &Binding,
|
||||
scope: &Scope,
|
||||
semantic: &SemanticModel,
|
||||
) -> Option<Diagnostic> {
|
||||
if binding.is_used() || !binding.kind.is_assignment() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let source = binding.source?;
|
||||
match semantic.statement(source) {
|
||||
Stmt::Assign(ast::StmtAssign { value, targets, .. }) if targets.len() == 1 => {
|
||||
asyncio_dangling_task(value, semantic)
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
for binding_id in scope.binding_ids() {
|
||||
// If the binding itself is used, or it's not an assignment, skip it.
|
||||
let binding = semantic.binding(binding_id);
|
||||
if binding.is_used() || !binding.kind.is_assignment() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, any dangling tasks, including those that are shadowed, as in:
|
||||
// ```python
|
||||
// if x > 0:
|
||||
// task = asyncio.create_task(make_request())
|
||||
// else:
|
||||
// task = asyncio.create_task(make_request())
|
||||
// ```
|
||||
for binding_id in
|
||||
std::iter::successors(Some(binding_id), |id| semantic.shadowed_binding(*id))
|
||||
{
|
||||
let binding = semantic.binding(binding_id);
|
||||
if binding.is_used() || !binding.kind.is_assignment() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(source) = binding.source else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let diagnostic = match semantic.statement(source) {
|
||||
Stmt::Assign(ast::StmtAssign { value, targets, .. }) if targets.len() == 1 => {
|
||||
asyncio_dangling_task(value, semantic)
|
||||
}
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
value: Some(value), ..
|
||||
}) => asyncio_dangling_task(value, semantic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(diagnostic) = diagnostic {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
value: Some(value), ..
|
||||
}) => asyncio_dangling_task(value, semantic),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr};
|
||||
|
||||
use ruff_python_ast::helpers::{map_callable, map_subscript};
|
||||
use ruff_python_semantic::{BindingKind, SemanticModel};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::{analyze, BindingKind, SemanticModel};
|
||||
|
||||
/// Return `true` if the given [`Expr`] is a special class attribute, like `__slots__`.
|
||||
///
|
||||
@@ -57,19 +56,13 @@ pub(super) fn has_default_copy_semantics(
|
||||
class_def: &ast::StmtClassDef,
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
let Some(Arguments { args: bases, .. }) = class_def.arguments.as_deref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
bases.iter().any(|expr| {
|
||||
semantic.resolve_call_path(expr).is_some_and(|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["pydantic", "BaseModel" | "BaseSettings"]
|
||||
| ["pydantic_settings", "BaseSettings"]
|
||||
| ["msgspec", "Struct"]
|
||||
)
|
||||
})
|
||||
analyze::class::any_over_body(class_def, semantic, &|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["pydantic", "BaseModel" | "BaseSettings"]
|
||||
| ["pydantic_settings", "BaseSettings"]
|
||||
| ["msgspec", "Struct"]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
19
crates/ruff_notebook/resources/test/fixtures/jupyter/cell/unicode_magic_gh9145.json
vendored
Normal file
19
crates/ruff_notebook/resources/test/fixtures/jupyter/cell/unicode_magic_gh9145.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"execution_count": null,
|
||||
"cell_type": "code",
|
||||
"id": "1",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def sample_func(xx):\n",
|
||||
" \"\"\"\n",
|
||||
" 转置 (transpose)\n",
|
||||
" \"\"\"\n",
|
||||
" return xx.T",
|
||||
"# https://github.com/astral-sh/ruff-vscode/issues/362",
|
||||
"DEFAULT_SYSTEM_PROMPT = (",
|
||||
" \"Ты — Сайга, русскоязычный автоматический ассистент. \"",
|
||||
" \"Ты разговариваешь с людьми и помогаешь им.\"",
|
||||
")"
|
||||
]
|
||||
}
|
||||
@@ -171,48 +171,43 @@ impl Cell {
|
||||
|
||||
// Detect cell magics (which operate on multiple lines).
|
||||
lines.any(|line| {
|
||||
line.split_whitespace().next().is_some_and(|first| {
|
||||
if first.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
let (token, command) = first.split_at(2);
|
||||
// These cell magics are special in that the lines following them are valid
|
||||
// Python code and the variables defined in that scope are available to the
|
||||
// rest of the notebook.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// Cell 1:
|
||||
// ```python
|
||||
// x = 1
|
||||
// ```
|
||||
//
|
||||
// Cell 2:
|
||||
// ```python
|
||||
// %%time
|
||||
// y = x
|
||||
// ```
|
||||
//
|
||||
// Cell 3:
|
||||
// ```python
|
||||
// print(y) # Here, `y` is available.
|
||||
// ```
|
||||
//
|
||||
// This is to avoid false positives when these variables are referenced
|
||||
// elsewhere in the notebook.
|
||||
token == "%%"
|
||||
&& !matches!(
|
||||
command,
|
||||
"capture"
|
||||
| "debug"
|
||||
| "prun"
|
||||
| "pypy"
|
||||
| "python"
|
||||
| "python3"
|
||||
| "time"
|
||||
| "timeit"
|
||||
)
|
||||
})
|
||||
let Some(first) = line.split_whitespace().next() else {
|
||||
return false;
|
||||
};
|
||||
if first.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
let Some(command) = first.strip_prefix("%%") else {
|
||||
return false;
|
||||
};
|
||||
// These cell magics are special in that the lines following them are valid
|
||||
// Python code and the variables defined in that scope are available to the
|
||||
// rest of the notebook.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// Cell 1:
|
||||
// ```python
|
||||
// x = 1
|
||||
// ```
|
||||
//
|
||||
// Cell 2:
|
||||
// ```python
|
||||
// %%time
|
||||
// y = x
|
||||
// ```
|
||||
//
|
||||
// Cell 3:
|
||||
// ```python
|
||||
// print(y) # Here, `y` is available.
|
||||
// ```
|
||||
//
|
||||
// This is to avoid false positives when these variables are referenced
|
||||
// elsewhere in the notebook.
|
||||
!matches!(
|
||||
command,
|
||||
"capture" | "debug" | "prun" | "pypy" | "python" | "python3" | "time" | "timeit"
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,17 +421,18 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test_case(Path::new("markdown.json"), false; "markdown")]
|
||||
#[test_case(Path::new("only_magic.json"), true; "only_magic")]
|
||||
#[test_case(Path::new("code_and_magic.json"), true; "code_and_magic")]
|
||||
#[test_case(Path::new("only_code.json"), true; "only_code")]
|
||||
#[test_case(Path::new("cell_magic.json"), false; "cell_magic")]
|
||||
#[test_case(Path::new("valid_cell_magic.json"), true; "valid_cell_magic")]
|
||||
#[test_case(Path::new("automagic.json"), false; "automagic")]
|
||||
#[test_case(Path::new("automagics.json"), false; "automagics")]
|
||||
#[test_case(Path::new("automagic_before_code.json"), false; "automagic_before_code")]
|
||||
#[test_case(Path::new("automagic_after_code.json"), true; "automagic_after_code")]
|
||||
fn test_is_valid_code_cell(path: &Path, expected: bool) -> Result<()> {
|
||||
#[test_case("markdown", false)]
|
||||
#[test_case("only_magic", true)]
|
||||
#[test_case("code_and_magic", true)]
|
||||
#[test_case("only_code", true)]
|
||||
#[test_case("cell_magic", false)]
|
||||
#[test_case("valid_cell_magic", true)]
|
||||
#[test_case("automagic", false)]
|
||||
#[test_case("automagics", false)]
|
||||
#[test_case("automagic_before_code", false)]
|
||||
#[test_case("automagic_after_code", true)]
|
||||
#[test_case("unicode_magic_gh9145", true)]
|
||||
fn test_is_valid_code_cell(cell: &str, expected: bool) -> Result<()> {
|
||||
/// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory.
|
||||
fn read_jupyter_cell(path: impl AsRef<Path>) -> Result<Cell> {
|
||||
let path = notebook_path("cell").join(path);
|
||||
@@ -439,7 +440,10 @@ mod tests {
|
||||
Ok(serde_json::from_str(&source_code)?)
|
||||
}
|
||||
|
||||
assert_eq!(read_jupyter_cell(path)?.is_valid_code_cell(), expected);
|
||||
assert_eq!(
|
||||
read_jupyter_cell(format!("{cell}.json"))?.is_valid_code_cell(),
|
||||
expected
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -921,178 +921,204 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the function has an implicit return.
|
||||
pub fn implicit_return(function: &ast::StmtFunctionDef) -> bool {
|
||||
/// Returns `true` if the body may break via a `break` statement.
|
||||
fn sometimes_breaks(stmts: &[Stmt]) -> bool {
|
||||
for stmt in stmts {
|
||||
match stmt {
|
||||
Stmt::For(ast::StmtFor { body, orelse, .. }) => {
|
||||
if returns(body) {
|
||||
return false;
|
||||
}
|
||||
if sometimes_breaks(orelse) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
|
||||
if returns(body) {
|
||||
return false;
|
||||
}
|
||||
if sometimes_breaks(orelse) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::If(ast::StmtIf {
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
}) => {
|
||||
if std::iter::once(body)
|
||||
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
|
||||
.any(|body| sometimes_breaks(body))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::Match(ast::StmtMatch { cases, .. }) => {
|
||||
if cases.iter().any(|case| sometimes_breaks(&case.body)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
..
|
||||
}) => {
|
||||
if sometimes_breaks(body)
|
||||
|| handlers.iter().any(|handler| {
|
||||
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
|
||||
body,
|
||||
..
|
||||
}) = handler;
|
||||
sometimes_breaks(body)
|
||||
})
|
||||
|| sometimes_breaks(orelse)
|
||||
|| sometimes_breaks(finalbody)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::With(ast::StmtWith { body, .. }) => {
|
||||
if sometimes_breaks(body) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::Break(_) => return true,
|
||||
Stmt::Return(_) => return false,
|
||||
Stmt::Raise(_) => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Terminal {
|
||||
/// Every path through the function ends with a `raise` statement.
|
||||
Raise,
|
||||
/// Every path through the function ends with a `return` (or `raise`) statement.
|
||||
Return,
|
||||
}
|
||||
|
||||
/// Returns `true` if the body may break via a `break` statement.
|
||||
fn always_breaks(stmts: &[Stmt]) -> bool {
|
||||
for stmt in stmts {
|
||||
match stmt {
|
||||
Stmt::Break(_) => return true,
|
||||
Stmt::Return(_) => return false,
|
||||
Stmt::Raise(_) => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns `true` if the body contains a branch that ends without an explicit `return` or
|
||||
/// `raise` statement.
|
||||
fn returns(stmts: &[Stmt]) -> bool {
|
||||
for stmt in stmts.iter().rev() {
|
||||
match stmt {
|
||||
Stmt::For(ast::StmtFor { body, orelse, .. }) => {
|
||||
if always_breaks(body) {
|
||||
return false;
|
||||
impl Terminal {
|
||||
/// Returns the [`Terminal`] behavior of the function, if it can be determined, or `None` if the
|
||||
/// function contains at least one control flow path that does not end with a `return` or `raise`
|
||||
/// statement.
|
||||
pub fn from_function(function: &ast::StmtFunctionDef) -> Option<Terminal> {
|
||||
/// Returns `true` if the body may break via a `break` statement.
|
||||
fn sometimes_breaks(stmts: &[Stmt]) -> bool {
|
||||
for stmt in stmts {
|
||||
match stmt {
|
||||
Stmt::For(ast::StmtFor { body, orelse, .. }) => {
|
||||
if returns(body).is_some() {
|
||||
return false;
|
||||
}
|
||||
if sometimes_breaks(orelse) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if returns(body) {
|
||||
return true;
|
||||
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
|
||||
if returns(body).is_some() {
|
||||
return false;
|
||||
}
|
||||
if sometimes_breaks(orelse) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if returns(orelse) && !sometimes_breaks(body) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
|
||||
if always_breaks(body) {
|
||||
return false;
|
||||
}
|
||||
if returns(body) {
|
||||
return true;
|
||||
}
|
||||
if returns(orelse) && !sometimes_breaks(body) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::If(ast::StmtIf {
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
}) => {
|
||||
if elif_else_clauses.iter().any(|clause| clause.test.is_none())
|
||||
&& std::iter::once(body)
|
||||
Stmt::If(ast::StmtIf {
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
}) => {
|
||||
if std::iter::once(body)
|
||||
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
|
||||
.all(|body| returns(body))
|
||||
{
|
||||
return true;
|
||||
.any(|body| sometimes_breaks(body))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::Match(ast::StmtMatch { cases, .. }) => {
|
||||
if cases.iter().any(|case| sometimes_breaks(&case.body)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
..
|
||||
}) => {
|
||||
if sometimes_breaks(body)
|
||||
|| handlers.iter().any(|handler| {
|
||||
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
|
||||
body,
|
||||
..
|
||||
}) = handler;
|
||||
sometimes_breaks(body)
|
||||
})
|
||||
|| sometimes_breaks(orelse)
|
||||
|| sometimes_breaks(finalbody)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::With(ast::StmtWith { body, .. }) => {
|
||||
if sometimes_breaks(body) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::Break(_) => return true,
|
||||
Stmt::Return(_) => return false,
|
||||
Stmt::Raise(_) => return false,
|
||||
_ => {}
|
||||
}
|
||||
Stmt::Match(ast::StmtMatch { cases, .. }) => {
|
||||
// Note: we assume the `match` is exhaustive.
|
||||
if cases.iter().all(|case| returns(&case.body)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
..
|
||||
}) => {
|
||||
// If the `finally` block returns, the `try` block must also return.
|
||||
if returns(finalbody) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the `body` or the `else` block returns, the `try` block must also return.
|
||||
if (returns(body) || returns(orelse))
|
||||
&& handlers.iter().all(|handler| {
|
||||
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
|
||||
body,
|
||||
..
|
||||
}) = handler;
|
||||
returns(body)
|
||||
})
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::With(ast::StmtWith { body, .. }) => {
|
||||
if returns(body) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::Return(_) => return true,
|
||||
Stmt::Raise(_) => return true,
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
}
|
||||
false
|
||||
|
||||
/// Returns `true` if the body may break via a `break` statement.
|
||||
fn always_breaks(stmts: &[Stmt]) -> bool {
|
||||
for stmt in stmts {
|
||||
match stmt {
|
||||
Stmt::Break(_) => return true,
|
||||
Stmt::Return(_) => return false,
|
||||
Stmt::Raise(_) => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns `true` if the body contains a branch that ends without an explicit `return` or
|
||||
/// `raise` statement.
|
||||
fn returns(stmts: &[Stmt]) -> Option<Terminal> {
|
||||
for stmt in stmts.iter().rev() {
|
||||
match stmt {
|
||||
Stmt::For(ast::StmtFor { body, orelse, .. })
|
||||
| Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
|
||||
if always_breaks(body) {
|
||||
return None;
|
||||
}
|
||||
if let Some(terminal) = returns(body) {
|
||||
return Some(terminal);
|
||||
}
|
||||
if !sometimes_breaks(body) {
|
||||
if let Some(terminal) = returns(orelse) {
|
||||
return Some(terminal);
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::If(ast::StmtIf {
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
}) => {
|
||||
if elif_else_clauses.iter().any(|clause| clause.test.is_none()) {
|
||||
match Terminal::combine(std::iter::once(returns(body)).chain(
|
||||
elif_else_clauses.iter().map(|clause| returns(&clause.body)),
|
||||
)) {
|
||||
Some(Terminal::Raise) => return Some(Terminal::Raise),
|
||||
Some(Terminal::Return) => return Some(Terminal::Return),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::Match(ast::StmtMatch { cases, .. }) => {
|
||||
// Note: we assume the `match` is exhaustive.
|
||||
match Terminal::combine(cases.iter().map(|case| returns(&case.body))) {
|
||||
Some(Terminal::Raise) => return Some(Terminal::Raise),
|
||||
Some(Terminal::Return) => return Some(Terminal::Return),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
..
|
||||
}) => {
|
||||
// If the `finally` block returns, the `try` block must also return.
|
||||
if let Some(terminal) = returns(finalbody) {
|
||||
return Some(terminal);
|
||||
}
|
||||
|
||||
// If the body returns, the `try` block must also return.
|
||||
if returns(body) == Some(Terminal::Return) {
|
||||
return Some(Terminal::Return);
|
||||
}
|
||||
|
||||
// If the else block and all the handlers return, the `try` block must also
|
||||
// return.
|
||||
if let Some(terminal) =
|
||||
Terminal::combine(std::iter::once(returns(orelse)).chain(
|
||||
handlers.iter().map(|handler| {
|
||||
let ExceptHandler::ExceptHandler(
|
||||
ast::ExceptHandlerExceptHandler { body, .. },
|
||||
) = handler;
|
||||
returns(body)
|
||||
}),
|
||||
))
|
||||
{
|
||||
return Some(terminal);
|
||||
}
|
||||
}
|
||||
Stmt::With(ast::StmtWith { body, .. }) => {
|
||||
if let Some(terminal) = returns(body) {
|
||||
return Some(terminal);
|
||||
}
|
||||
}
|
||||
Stmt::Return(_) => return Some(Terminal::Return),
|
||||
Stmt::Raise(_) => return Some(Terminal::Raise),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
returns(&function.body)
|
||||
}
|
||||
|
||||
!returns(&function.body)
|
||||
/// Combine a series of [`Terminal`] operators.
|
||||
fn combine(iter: impl Iterator<Item = Option<Terminal>>) -> Option<Terminal> {
|
||||
iter.reduce(|acc, terminal| match (acc, terminal) {
|
||||
(Some(Self::Raise), Some(Self::Raise)) => Some(Self::Raise),
|
||||
(Some(_), Some(Self::Return)) => Some(Self::Return),
|
||||
(Some(Self::Return), Some(_)) => Some(Self::Return),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`StatementVisitor`] that collects all `raise` statements in a function or method.
|
||||
|
||||
@@ -33,9 +33,15 @@ node_lines = (
|
||||
nodes = []
|
||||
for node_line in node_lines:
|
||||
node = node_line.split("(")[1].split(")")[0].split("::")[-1].split("<")[0]
|
||||
# These nodes aren't used in the formatter as the formatting of them is handled
|
||||
# in one of the other nodes containing them.
|
||||
if node in ("FStringLiteralElement", "FStringExpressionElement"):
|
||||
# `FString` and `StringLiteral` has a custom implementation while the formatting for
|
||||
# `FStringLiteralElement` and `FStringExpressionElement` are handled by the `FString`
|
||||
# implementation.
|
||||
if node in (
|
||||
"FString",
|
||||
"StringLiteral",
|
||||
"FStringLiteralElement",
|
||||
"FStringExpressionElement",
|
||||
):
|
||||
continue
|
||||
nodes.append(node)
|
||||
print(nodes)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"preview": "enabled"
|
||||
}
|
||||
]
|
||||
38
crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.py
vendored
Normal file
38
crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
class NormalDocstring:
|
||||
|
||||
"""This is a docstring."""
|
||||
|
||||
|
||||
class DocstringWithComment0:
|
||||
# This is a comment
|
||||
"""This is a docstring."""
|
||||
|
||||
|
||||
class DocstringWithComment1:
|
||||
# This is a comment
|
||||
|
||||
"""This is a docstring."""
|
||||
|
||||
|
||||
class DocstringWithComment2:
|
||||
|
||||
# This is a comment
|
||||
"""This is a docstring."""
|
||||
|
||||
|
||||
class DocstringWithComment3:
|
||||
|
||||
# This is a comment
|
||||
|
||||
"""This is a docstring."""
|
||||
|
||||
|
||||
class DocstringWithComment4:
|
||||
|
||||
|
||||
# This is a comment
|
||||
|
||||
|
||||
"""This is a docstring."""
|
||||
|
||||
|
||||
@@ -170,3 +170,52 @@ class Abcdefghijklmopqrstuvwxyz(Abc, Def, Ghi, Jkl, Mno, Pqr, Stu, Vwx, Yz, A1,
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/9126
|
||||
def doctest_extra_indent1():
|
||||
"""
|
||||
Docstring example containing a class.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> @pl.api.register_dataframe_namespace("split")
|
||||
... class SplitFrame:
|
||||
... def __init__(self, df: pl.DataFrame):
|
||||
... self._df = df
|
||||
...
|
||||
... def by_first_letter_of_column_values(self, col: str) -> list[pl.DataFrame]:
|
||||
... return [
|
||||
... self._df.filter(pl.col(col).str.starts_with(c))
|
||||
... for c in sorted(
|
||||
... set(df.select(pl.col(col).str.slice(0, 1)).to_series())
|
||||
... )
|
||||
... ]
|
||||
"""
|
||||
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/9126
|
||||
class DoctestExtraIndent2:
|
||||
def example2():
|
||||
"""
|
||||
Regular docstring of class method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> df = pl.DataFrame(
|
||||
... {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}
|
||||
... )
|
||||
"""
|
||||
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/9126
|
||||
def doctest_extra_indent3():
|
||||
"""
|
||||
Pragma comment.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> af1, af2, af3 = pl.align_frames(
|
||||
... df1, df2, df3, on="dt"
|
||||
... ) # doctest: +IGNORE_RESULT
|
||||
"""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user