Compare commits

..

1 Commits

Author SHA1 Message Date
Dhruv Manilawala
b9d133f718 Update ecosystem with notebook repositories 2023-12-14 09:21:16 -06:00
165 changed files with 1544 additions and 5840 deletions

View File

@@ -226,7 +226,7 @@ jobs:
name: ruff
path: target/debug
- uses: dawidd6/action-download-artifact@v3
- uses: dawidd6/action-download-artifact@v2
name: Download baseline Ruff binary
with:
name: ruff

View File

@@ -17,7 +17,7 @@ jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: dawidd6/action-download-artifact@v3
- uses: dawidd6/action-download-artifact@v2
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@v3
- uses: dawidd6/action-download-artifact@v2
name: "Download ecosystem results"
id: download-ecosystem-result
if: steps.pr-number.outputs.pr-number

View File

@@ -1,42 +1,5 @@
# 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

View File

@@ -1,62 +1,5 @@
# Changelog
## 0.1.8
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/ruff-v0.1.8) for more details!
### Preview features
- Add `"preserve"` quote-style to mimic Black's skip-string-normalization ([#8822](https://github.com/astral-sh/ruff/pull/8822))
- Implement `prefer_splitting_right_hand_side_of_assignments` preview style ([#8943](https://github.com/astral-sh/ruff/pull/8943))
- \[`pycodestyle`\] Add fix for `unexpected-spaces-around-keyword-parameter-equals` ([#9072](https://github.com/astral-sh/ruff/pull/9072))
- \[`pycodestyle`\] Add fix for comment-related whitespace rules ([#9075](https://github.com/astral-sh/ruff/pull/9075))
- \[`pycodestyle`\] Allow `sys.path` modifications between imports ([#9047](https://github.com/astral-sh/ruff/pull/9047))
- \[`refurb`\] Implement `hashlib-digest-hex` (`FURB181`) ([#9077](https://github.com/astral-sh/ruff/pull/9077))
### Rule changes
- Allow `flake8-type-checking` rules to automatically quote runtime-evaluated references ([#6001](https://github.com/astral-sh/ruff/pull/6001))
- Allow transparent cell magics in Jupyter Notebooks ([#8911](https://github.com/astral-sh/ruff/pull/8911))
- \[`flake8-annotations`\] Avoid `ANN2xx` fixes for abstract methods with empty bodies ([#9034](https://github.com/astral-sh/ruff/pull/9034))
- \[`flake8-self`\] Ignore underscore references in type annotations ([#9036](https://github.com/astral-sh/ruff/pull/9036))
- \[`pep8-naming`\] Allow class names when `apps.get_model` is a non-string ([#9065](https://github.com/astral-sh/ruff/pull/9065))
- \[`pycodestyle`\] Allow `matplotlib.use` calls to intersperse imports ([#9094](https://github.com/astral-sh/ruff/pull/9094))
- \[`pyflakes`\] Support fixing unused assignments in tuples by renaming variables (`F841`) ([#9107](https://github.com/astral-sh/ruff/pull/9107))
- \[`pylint`\] Add fix for `subprocess-run-without-check` (`PLW1510`) ([#6708](https://github.com/astral-sh/ruff/pull/6708))
### Formatter
- Add `docstring-code-format` knob to enable docstring snippet formatting ([#8854](https://github.com/astral-sh/ruff/pull/8854))
- Use double quotes for all docstrings, including single-quoted docstrings ([#9020](https://github.com/astral-sh/ruff/pull/9020))
- Implement "dynamic" line width mode for docstring code formatting ([#9098](https://github.com/astral-sh/ruff/pull/9098))
- Support reformatting Markdown code blocks ([#9030](https://github.com/astral-sh/ruff/pull/9030))
- add support for formatting reStructuredText code snippets ([#9003](https://github.com/astral-sh/ruff/pull/9003))
- Avoid trailing comma for single-argument with positional separator ([#9076](https://github.com/astral-sh/ruff/pull/9076))
- Fix handling of trailing target comment ([#9051](https://github.com/astral-sh/ruff/pull/9051))
### CLI
- Hide unsafe fix suggestions when explicitly disabled ([#9095](https://github.com/astral-sh/ruff/pull/9095))
- Add SARIF support to `--output-format` ([#9078](https://github.com/astral-sh/ruff/pull/9078))
### Bug fixes
- Apply unnecessary index rule prior to enumerate rewrite ([#9012](https://github.com/astral-sh/ruff/pull/9012))
- \[`flake8-err-msg`\] Allow `EM` fixes even if `msg` variable is defined ([#9059](https://github.com/astral-sh/ruff/pull/9059))
- \[`flake8-pie`\] Prevent keyword arguments duplication ([#8450](https://github.com/astral-sh/ruff/pull/8450))
- \[`flake8-pie`\] Respect trailing comma in `unnecessary-dict-kwargs` (`PIE804`) ([#9015](https://github.com/astral-sh/ruff/pull/9015))
- \[`flake8-raise`\] Avoid removing parentheses on ctypes.WinError ([#9027](https://github.com/astral-sh/ruff/pull/9027))
- \[`isort`\] Avoid invalid combination of `force-sort-within-types` and `lines-between-types` ([#9041](https://github.com/astral-sh/ruff/pull/9041))
- \[`isort`\] Ensure that from-style imports are always ordered first in `__future__` ([#9039](https://github.com/astral-sh/ruff/pull/9039))
- \[`pycodestyle`\] Allow tab indentation before keyword ([#9099](https://github.com/astral-sh/ruff/pull/9099))
- \[`pylint`\] Ignore `@overrides` and `@overloads` for `too-many-positional` ([#9000](https://github.com/astral-sh/ruff/pull/9000))
- \[`pyupgrade`\] Enable `printf-string-formatting` fix with comments on right-hand side ([#9037](https://github.com/astral-sh/ruff/pull/9037))
- \[`refurb`\] Make `math-constant` (`FURB152`) rule more targeted ([#9054](https://github.com/astral-sh/ruff/pull/9054))
- \[`refurb`\] Support floating-point base in `redundant-log-base` (`FURB163`) ([#9100](https://github.com/astral-sh/ruff/pull/9100))
- \[`ruff`\] Detect `unused-asyncio-dangling-task` (`RUF006`) on unused assignments ([#9060](https://github.com/astral-sh/ruff/pull/9060))
## 0.1.7
### Preview features

View File

@@ -556,10 +556,10 @@ examples.
#### Linux
Install `perf` and build `ruff_benchmark` with the `profiling` profile and then run it with perf
Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf
```shell
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
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
```
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=profiling
perf record -g -F 999 target/profiling/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null
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
```
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 profiling -p ruff_benchmark -- --profile-time=1
cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1
```
- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc`

73
Cargo.lock generated
View File

@@ -809,7 +809,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.1.8"
version = "0.1.7"
dependencies = [
"anyhow",
"clap",
@@ -828,7 +828,7 @@ dependencies = [
"serde_json",
"strum",
"strum_macros",
"toml",
"toml 0.7.8",
]
[[package]]
@@ -1482,9 +1482,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "once_cell"
version = "1.19.0"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "oorandom"
@@ -1811,7 +1811,7 @@ dependencies = [
"pep440_rs",
"pep508_rs",
"serde",
"toml",
"toml 0.8.2",
]
[[package]]
@@ -2063,7 +2063,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.1.8"
version = "0.1.7"
dependencies = [
"annotate-snippets 0.9.2",
"anyhow",
@@ -2155,7 +2155,7 @@ dependencies = [
"strum",
"strum_macros",
"tempfile",
"toml",
"toml 0.7.8",
"tracing",
"tracing-indicatif",
"tracing-subscriber",
@@ -2199,7 +2199,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.8"
version = "0.1.7"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2255,7 +2255,7 @@ dependencies = [
"tempfile",
"test-case",
"thiserror",
"toml",
"toml 0.7.8",
"typed-arena",
"unicode-width",
"unicode_names2",
@@ -2452,7 +2452,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.8"
version = "0.1.7"
dependencies = [
"anyhow",
"clap",
@@ -2543,7 +2543,7 @@ dependencies = [
"shellexpand",
"strum",
"tempfile",
"toml",
"toml 0.7.8",
]
[[package]]
@@ -3091,6 +3091,18 @@ 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"
@@ -3100,7 +3112,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
"toml_edit 0.20.2",
]
[[package]]
@@ -3112,6 +3124,19 @@ 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"
@@ -3160,9 +3185,9 @@ dependencies = [
[[package]]
name = "tracing-indicatif"
version = "0.3.6"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069580424efe11d97c3fef4197fa98c004fa26672cc71ad8770d224e23b1951d"
checksum = "57e05fe4a1c906d94b275d8aeb8ff8b9deaca502aeb59ae8ab500a92b8032ac8"
dependencies = [
"indicatif",
"tracing",
@@ -3282,9 +3307,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "unicode_names2"
version = "1.2.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac64ef2f016dc69dfa8283394a70b057066eb054d5fcb6b9eb17bd2ec5097211"
checksum = "5d5506ae2c3c1ccbdf468e52fc5ef536c2ccd981f01273a4cb81aa61021f3a5f"
dependencies = [
"phf",
"unicode_names2_generator",
@@ -3292,9 +3317,9 @@ dependencies = [
[[package]]
name = "unicode_names2_generator"
version = "1.2.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "013f6a731e80f3930de580e55ba41dfa846de4e0fdee4a701f97989cb1597d6a"
checksum = "b6dfc680313e95bc6637fa278cd7a22390c3c2cd7b8b2bd28755bc6c0fc811e7"
dependencies = [
"getopts",
"log",
@@ -3463,9 +3488,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.39"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12"
checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
dependencies = [
"cfg-if",
"js-sys",
@@ -3504,9 +3529,9 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
[[package]]
name = "wasm-bindgen-test"
version = "0.3.39"
version = "0.3.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403"
checksum = "c6433b7c56db97397842c46b67e11873eda263170afeb3a2dc74a7cb370fee0d"
dependencies = [
"console_error_panic_hook",
"js-sys",
@@ -3518,9 +3543,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.39"
version = "0.3.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89"
checksum = "493fcbab756bb764fa37e6bee8cec2dd709eb4273d06d0c282a5e74275ded735"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -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.19.0" }
once_cell = { version = "1.17.1" }
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.8.2" }
toml = { version = "0.7.8" }
tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.6" }
tracing-indicatif = { version = "0.3.4" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
unicode-ident = { version = "1.0.12" }
unicode_names2 = { version = "1.2.1" }
unicode_names2 = { version = "1.2.0" }
unicode-width = { version = "0.1.11" }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
@@ -88,20 +88,7 @@ rc_mutex = "warn"
rest_pat_in_fully_bound_structs = "warn"
[profile.release]
# 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]
lto = "fat"
codegen-units = 1
[profile.dev.package.insta]
@@ -115,8 +102,8 @@ opt-level = 3
[profile.dev.package.ruff_python_parser]
opt-level = 1
# Use the `--profile profiling` flag to show symbols in release mode.
# e.g. `cargo build --profile profiling`
[profile.profiling]
# Use the `--profile release-debug` flag to show symbols in release mode.
# e.g. `cargo build --profile release-debug`
[profile.release-debug]
inherits = "release"
debug = 1

View File

@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.8
rev: v0.1.7
hooks:
# Run the linter.
- id: ruff
@@ -194,25 +194,20 @@ 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",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.1.8"
version = "0.1.7"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.1.8"
version = "0.1.7"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -515,7 +515,7 @@ impl<'a> FormatResults<'a> {
if changed > 0 && unchanged > 0 {
writeln!(
f,
"{} file{} {}, {} file{} {}",
"{} file{} {}, {} file{} left unchanged",
changed,
if changed == 1 { "" } else { "s" },
match self.mode {
@@ -524,10 +524,6 @@ 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!(
@@ -543,13 +539,9 @@ impl<'a> FormatResults<'a> {
} else if unchanged > 0 {
writeln!(
f,
"{} file{} {}",
"{} file{} left unchanged",
unchanged,
if unchanged == 1 { "" } else { "s" },
match self.mode {
FormatMode::Write => "left unchanged",
FormatMode::Check | FormatMode::Diff => "already formatted",
},
)
} else {
Ok(())

View File

@@ -139,99 +139,6 @@ if condition:
Ok(())
}
#[test]
fn docstring_options() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[format]
docstring-code-format = true
docstring-code-line-length = 20
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
def f(x):
'''
Something about `f`. And an example:
.. code-block:: python
foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
Another example:
```py
foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
```
And another:
>>> foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
'''
pass
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def f(x):
"""
Something about `f`. And an example:
.. code-block:: python
(
foo,
bar,
quux,
) = this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
Another example:
```py
(
foo,
bar,
quux,
) = this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
```
And another:
>>> (
... foo,
... bar,
... quux,
... ) = this_is_a_long_line(
... lion,
... hippo,
... lemur,
... bear,
... )
"""
pass
----- stderr -----
"###);
Ok(())
}
#[test]
fn mixed_line_endings() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -255,7 +162,7 @@ fn mixed_line_endings() -> Result<()> {
----- stdout -----
----- stderr -----
2 files already formatted
2 files left unchanged
"###);
Ok(())
}
@@ -328,60 +235,6 @@ 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()?;
@@ -930,7 +783,7 @@ fn test_diff() {
----- stderr -----
2 files would be reformatted, 1 file already formatted
2 files would be reformatted, 1 file left unchanged
"###);
});
}

View File

@@ -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, Hash)]
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Edit {
/// The start location of the edit.

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.1.8"
version = "0.1.7"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -182,33 +182,3 @@ 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

View File

@@ -127,21 +127,3 @@ 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)

View File

@@ -1,13 +1,8 @@
import typing
import typing_extensions
from typing import TypeVar
from typing_extensions import ParamSpec, TypeVarTuple
_T = typing.TypeVar("_T")
_Ts = typing_extensions.TypeVarTuple("_Ts")
_P = ParamSpec("_P")
_P2 = typing.ParamSpec("_P2")
_Ts2 = TypeVarTuple("_Ts2")
_P = TypeVar("_P")
# OK
_UsedTypeVar = TypeVar("_UsedTypeVar")

View File

@@ -1,13 +1,8 @@
import typing
import typing_extensions
from typing import TypeVar
from typing_extensions import ParamSpec, TypeVarTuple
_T = typing.TypeVar("_T")
_Ts = typing_extensions.TypeVarTuple("_Ts")
_P = ParamSpec("_P")
_P2 = typing.ParamSpec("_P2")
_Ts2 = TypeVarTuple("_Ts2")
_P = TypeVar("_P")
# OK
_UsedTypeVar = TypeVar("_UsedTypeVar")

View File

@@ -37,28 +37,3 @@ 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__
...

View File

@@ -1,5 +1,6 @@
# Errors
"yoda" == compare # SIM300
"yoda" == compare # SIM300
42 == age # SIM300
("a", "b") == compare # SIM300
"yoda" <= compare # SIM300
@@ -12,17 +13,10 @@ 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
@@ -37,6 +31,3 @@ 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

View File

@@ -1,18 +0,0 @@
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

View File

@@ -1,16 +0,0 @@
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

View File

@@ -1,7 +0,0 @@
"""Add `TYPE_CHECKING` to an existing `typing` import. Another member is moved."""
from __future__ import annotations
from typing import Final
Const: Final[dict] = {}

View File

@@ -1,7 +0,0 @@
"""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] = {}

View File

@@ -1,7 +0,0 @@
"""Using `TYPE_CHECKING` from an existing `typing` import. Another member is moved."""
from __future__ import annotations
from typing import Final, Mapping
Const: Final[dict] = {}

View File

@@ -65,28 +65,3 @@ 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]]:
...

View File

@@ -713,12 +713,5 @@ 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
"""
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:
And so is this, but it we should preserve the extra space on this line relative
"""

View File

@@ -1,4 +0,0 @@
import re
from typing import Annotated
type X = Annotated[int, lambda: re.compile("x")]

View File

@@ -1,36 +0,0 @@
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

View File

@@ -1,61 +0,0 @@
# 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

View File

@@ -122,33 +122,3 @@ 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

View File

@@ -59,11 +59,3 @@ 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]] = []

View File

@@ -3,11 +3,12 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint};
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint, ruff};
/// 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,
@@ -71,5 +72,12 @@ 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);
}
}
}
}

View File

@@ -2,7 +2,7 @@ use ruff_python_ast::Expr;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_pie, pylint, refurb};
use crate::rules::{flake8_pie, pylint};
/// Run lint rules over all deferred lambdas in the [`SemanticModel`].
pub(crate) fn deferred_lambdas(checker: &mut Checker) {
@@ -21,9 +21,6 @@ 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());
}
}
}
}

View File

@@ -5,21 +5,16 @@ 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, ruff,
};
use crate::rules::{flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint};
/// 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,
@@ -36,6 +31,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UnusedPrivateTypedDict,
Rule::UnusedStaticMethodArgument,
Rule::UnusedVariable,
Rule::NoSelfUse,
]) {
return;
}
@@ -273,10 +269,6 @@ 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);
@@ -344,10 +336,6 @@ 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);

View File

@@ -15,9 +15,8 @@ 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_type_checking, 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_use_pathlib, flynt, numpy,
pandas_vet, pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff,
};
use crate::settings::types::PythonVersion;
@@ -1171,9 +1170,6 @@ 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(

View File

@@ -368,9 +368,6 @@ 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) {
@@ -400,13 +397,27 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_django::rules::nullable_model_string_field(checker, body);
}
if checker.enabled(Rule::DjangoExcludeWithModelForm) {
flake8_django::rules::exclude_with_model_form(checker, class_def);
if let Some(diagnostic) = flake8_django::rules::exclude_with_model_form(
checker,
arguments.as_deref(),
body,
) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::DjangoAllWithModelForm) {
flake8_django::rules::all_with_model_form(checker, class_def);
if let Some(diagnostic) =
flake8_django::rules::all_with_model_form(checker, arguments.as_deref(), body)
{
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::DjangoUnorderedBodyContentInModel) {
flake8_django::rules::unordered_body_content_in_model(checker, class_def);
flake8_django::rules::unordered_body_content_in_model(
checker,
arguments.as_deref(),
body,
);
}
if !checker.source_type.is_stub() {
if checker.enabled(Rule::DjangoModelWithoutDunderStr) {

View File

@@ -2013,15 +2013,13 @@ pub(crate) fn check_ast(
// Iterate over the AST.
checker.visit_body(python_ast);
// 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.
// Visit any deferred syntax nodes.
checker.visit_deferred_functions();
checker.visit_deferred_type_param_definitions();
checker.visit_deferred_lambdas();
checker.visit_deferred_future_type_definitions();
checker.visit_deferred_type_param_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.

View File

@@ -252,7 +252,6 @@ 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),
@@ -808,7 +807,6 @@ 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),
@@ -953,7 +951,6 @@ 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)]

View File

@@ -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::{ImportedName, SemanticModel};
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::textwrap::indent;
use ruff_source_file::Locator;
@@ -132,48 +132,7 @@ impl<'a> Importer<'a> {
)?;
// Import the `TYPE_CHECKING` symbol from the typing module.
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)
};
let (type_checking_edit, type_checking) = self.get_or_import_type_checking(at, semantic)?;
// Add the import to a `TYPE_CHECKING` block.
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
@@ -198,21 +157,28 @@ impl<'a> Importer<'a> {
})
}
/// Find a reference to `typing.TYPE_CHECKING`.
fn find_type_checking(
/// 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,
at: TextSize,
semantic: &SemanticModel,
) -> Result<Option<ImportedName>, ResolutionError> {
) -> Result<(Edit, String), ResolutionError> {
for module in semantic.typing_modules() {
if let Some(imported_name) = Self::find_symbol(
if let Some((edit, name)) = self.get_symbol(
&ImportRequest::import_from(module, "TYPE_CHECKING"),
at,
semantic,
)? {
return Ok(Some(imported_name));
return Ok((edit, name));
}
}
Ok(None)
self.import_symbol(
&ImportRequest::import_from("typing", "TYPE_CHECKING"),
at,
semantic,
)
}
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
@@ -226,15 +192,16 @@ impl<'a> Importer<'a> {
semantic: &SemanticModel,
) -> Result<(Edit, String), ResolutionError> {
self.get_symbol(symbol, at, semantic)?
.map_or_else(|| self.import_symbol(symbol, at, None, semantic), Ok)
.map_or_else(|| self.import_symbol(symbol, at, semantic), Ok)
}
/// Return the [`ImportedName`] to for existing symbol, if it's present in the given [`SemanticModel`].
fn find_symbol(
/// 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<ImportedName>, ResolutionError> {
) -> Result<Option<(Edit, String)>, 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)
@@ -259,21 +226,6 @@ 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:
//
@@ -307,13 +259,9 @@ 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)
.filter(|stmt| except != Some(stmt))
{
if let Some(stmt) = self.find_import_from(symbol.module, at) {
// 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.
@@ -475,18 +423,14 @@ impl RuntimeImportEdit {
#[derive(Debug)]
pub(crate) struct TypingImportEdit {
/// The edit to add the `TYPE_CHECKING` symbol to the module.
type_checking_edit: Option<Edit>,
type_checking_edit: Edit,
/// The edit to add the import to a `TYPE_CHECKING` block.
add_import_edit: Edit,
}
impl TypingImportEdit {
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)
}
pub(crate) fn into_edits(self) -> Vec<Edit> {
vec![self.type_checking_edit, self.add_import_edit]
}
}

View File

@@ -3,7 +3,7 @@ use rustc_hash::FxHashSet;
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers::{
pep_604_union, typing_optional, typing_union, ReturnStatementVisitor, Terminal,
implicit_return, pep_604_union, typing_optional, typing_union, ReturnStatementVisitor,
};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, ExprContext};
@@ -57,14 +57,6 @@ 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));
@@ -88,7 +80,7 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
// if x > 0:
// return 1
// ```
if terminal.is_none() {
if implicit_return(function) {
return_type = return_type.union(ResolvedPythonType::Atom(PythonType::None));
}
@@ -102,7 +94,6 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
#[derive(Debug)]
pub(crate) enum AutoPythonType {
Never,
Atom(PythonType),
Union(FxHashSet<PythonType>),
}
@@ -120,28 +111,6 @@ 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![]))

View File

@@ -495,88 +495,4 @@ 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:

View File

@@ -550,96 +550,4 @@ 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:

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Expr};
@@ -6,7 +6,6 @@ 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.
@@ -29,25 +28,16 @@ use crate::fix::edits::add_argument;
/// 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 AlwaysFixableViolation for ZipWithoutExplicitStrict {
impl Violation 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
@@ -62,27 +52,9 @@ pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::Exp
.iter()
.any(|arg| is_infinite_iterator(arg, checker.semantic()))
{
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);
checker
.diagnostics
.push(Diagnostic::new(ZipWithoutExplicitStrict, call.range()));
}
}
}

View File

@@ -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,19 +9,8 @@ B905.py:4:1: B905 [*] `zip()` without an explicit `strict=` parameter
5 | zip(range(3))
6 | zip("a", "b")
|
= help: Add explicit `strict=False`
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
B905.py:5:1: B905 `zip()` without an explicit `strict=` parameter
|
3 | # Errors
4 | zip()
@@ -30,19 +19,8 @@ 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`
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
B905.py:6:1: B905 `zip()` without an explicit `strict=` parameter
|
4 | zip()
5 | zip(range(3))
@@ -51,19 +29,8 @@ 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`
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
B905.py:7:1: B905 `zip()` without an explicit `strict=` parameter
|
5 | zip(range(3))
6 | zip("a", "b")
@@ -72,19 +39,8 @@ 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`
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
B905.py:7:16: B905 `zip()` without an explicit `strict=` parameter
|
5 | zip(range(3))
6 | zip("a", "b")
@@ -93,19 +49,8 @@ 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`
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
B905.py:8:5: B905 `zip()` without an explicit `strict=` parameter
|
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
@@ -113,19 +58,8 @@ B905.py:8:5: B905 [*] `zip()` without an explicit `strict=` parameter
| ^^^^^^^^ B905
9 | zip(zip("a", strict=True))
|
= help: Add explicit `strict=False`
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
B905.py:9:1: B905 `zip()` without an explicit `strict=` parameter
|
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
@@ -134,49 +68,21 @@ B905.py:9:1: B905 [*] `zip()` without an explicit `strict=` parameter
10 |
11 | # OK
|
= help: Add explicit `strict=False`
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
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`
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
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)

View File

@@ -1,6 +1,7 @@
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;
@@ -47,12 +48,21 @@ impl Violation for DjangoAllWithModelForm {
}
/// DJ007
pub(crate) fn all_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !is_model_form(class_def, checker.semantic()) {
return;
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;
}
for element in &class_def.body {
for element in body {
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
continue;
};
@@ -73,18 +83,12 @@ pub(crate) fn all_with_model_form(checker: &mut Checker, class_def: &ast::StmtCl
match value.as_ref() {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if value == "__all__" {
checker
.diagnostics
.push(Diagnostic::new(DjangoAllWithModelForm, element.range()));
return;
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
}
}
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => {
if value == "__all__".as_bytes() {
checker
.diagnostics
.push(Diagnostic::new(DjangoAllWithModelForm, element.range()));
return;
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
}
}
_ => (),
@@ -92,4 +96,5 @@ pub(crate) fn all_with_model_form(checker: &mut Checker, class_def: &ast::StmtCl
}
}
}
None
}

View File

@@ -1,6 +1,7 @@
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;
@@ -45,12 +46,21 @@ impl Violation for DjangoExcludeWithModelForm {
}
/// DJ006
pub(crate) fn exclude_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !is_model_form(class_def, checker.semantic()) {
return;
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;
}
for element in &class_def.body {
for element in body {
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
continue;
};
@@ -66,12 +76,10 @@ pub(crate) fn exclude_with_model_form(checker: &mut Checker, class_def: &ast::St
continue;
};
if id == "exclude" {
checker
.diagnostics
.push(Diagnostic::new(DjangoExcludeWithModelForm, target.range()));
return;
return Some(Diagnostic::new(DjangoExcludeWithModelForm, target.range()));
}
}
}
}
None
}

View File

@@ -1,17 +1,17 @@
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::Expr;
use ruff_python_semantic::{analyze, SemanticModel};
use ruff_python_semantic::SemanticModel;
/// Return `true` if a Python class appears to be a Django model, based on its base classes.
pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
analyze::class::any_over_body(class_def, semantic, &|call_path| {
pub(super) fn is_model(base: &Expr, semantic: &SemanticModel) -> bool {
semantic.resolve_call_path(base).is_some_and(|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(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
analyze::class::any_over_body(class_def, semantic, &|call_path| {
pub(super) fn is_model_form(base: &Expr, semantic: &SemanticModel) -> bool {
semantic.resolve_call_path(base).is_some_and(|call_path| {
matches!(
call_path.as_slice(),
["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"]

View File

@@ -1,9 +1,10 @@
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;
@@ -51,39 +52,57 @@ impl Violation for DjangoModelWithoutDunderStr {
}
/// DJ008
pub(crate) fn model_without_dunder_str(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !is_non_abstract_model(class_def, checker.semantic()) {
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()) {
return;
}
if has_dunder_method(class_def) {
if has_dunder_method(body) {
return;
}
checker.diagnostics.push(Diagnostic::new(
DjangoModelWithoutDunderStr,
class_def.identifier(),
));
checker
.diagnostics
.push(Diagnostic::new(DjangoModelWithoutDunderStr, name.range()));
}
/// 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__",
fn has_dunder_method(body: &[Stmt]) -> bool {
body.iter().any(|val| match val {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => {
if name == "__str__" {
return true;
}
false
}
_ => 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)
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;
}
bases.iter().any(|base| helpers::is_model(base, semantic))
}
/// Check if class is abstract, in terms of Django model inheritance.
fn is_model_abstract(class_def: &ast::StmtClassDef) -> bool {
for element in &class_def.body {
fn is_model_abstract(body: &[Stmt]) -> bool {
for element in body {
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
continue;
};

View File

@@ -1,8 +1,9 @@
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;
@@ -78,50 +79,6 @@ 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,
@@ -183,3 +140,53 @@ 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);
}
}
}

View File

@@ -54,12 +54,4 @@ 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
|

View File

@@ -1,10 +1,10 @@
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::{self 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,24 +28,14 @@ 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
@@ -75,29 +65,19 @@ 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 mut diagnostic = {
let diagnostic = {
let call_path = checker.semantic().resolve_call_path(&call.func);
if call_path
.as_ref()
@@ -133,15 +113,5 @@ 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);
}

View File

@@ -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,18 +10,8 @@ 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`
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
T201.py:5:1: T201 `print` found
|
4 | print("Hello, world!") # T201
5 | print("Hello, world!", file=None) # T201
@@ -29,18 +19,8 @@ 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`
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
T201.py:6:1: T201 `print` found
|
4 | print("Hello, world!") # T201
5 | print("Hello, world!", file=None) # T201
@@ -48,18 +28,8 @@ T201.py:6:1: T201 [*] `print` found
| ^^^^^ T201
7 | print("Hello, world!", file=sys.stderr) # T201
|
= help: Remove `print`
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
T201.py:7:1: T201 `print` found
|
5 | print("Hello, world!", file=None) # T201
6 | print("Hello, world!", file=sys.stdout) # T201
@@ -68,15 +38,5 @@ 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

View File

@@ -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,17 +10,8 @@ T203.py:3:1: T203 [*] `pprint` found
4 |
5 | import pprint
|
= help: Remove `pprint`
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
T203.py:7:1: T203 `pprint` found
|
5 | import pprint
6 |
@@ -29,14 +20,5 @@ 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!")

View File

@@ -80,24 +80,17 @@ 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 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);
}
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);
}
};
@@ -120,82 +113,55 @@ 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 {
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(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(),
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(),
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
})),
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(),
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()
.into_iter()
.map(std::convert::AsRef::as_ref)
.collect(),
)),
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(

View File

@@ -7,35 +7,28 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the presence of unused private `TypeVar`, `ParamSpec` or
/// `TypeVarTuple` declarations.
/// Checks for the presence of unused private `TypeVar` declarations.
///
/// ## Why is this bad?
/// A private `TypeVar` that is defined but not used is likely a mistake. It
/// A private `TypeVar` that is defined but not used is likely a mistake, and
/// 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 {
type_var_like_name: String,
type_var_like_kind: String,
name: String,
}
impl Violation for UnusedPrivateTypeVar {
#[derive_message_formats]
fn message(&self) -> String {
let UnusedPrivateTypeVar {
type_var_like_name,
type_var_like_kind,
} = self;
format!("Private {type_var_like_kind} `{type_var_like_name}` is never used")
let UnusedPrivateTypeVar { name } = self;
format!("Private TypeVar `{name}` is never used")
}
}
@@ -192,26 +185,13 @@ pub(crate) fn unused_private_type_var(
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
continue;
};
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 {
if !checker.semantic().match_typing_expr(func, "TypeVar") {
continue;
};
}
diagnostics.push(Diagnostic::new(
UnusedPrivateTypeVar {
type_var_like_name: id.to_string(),
type_var_like_kind: type_var_like_kind.to_string(),
name: id.to_string(),
},
binding.range(),
));

View File

@@ -1,52 +1,22 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI018.py:6:1: PYI018 Private TypeVar `_T` is never used
PYI018.py:4:1: PYI018 Private TypeVar `_T` is never used
|
4 | from typing_extensions import ParamSpec, TypeVarTuple
5 |
6 | _T = typing.TypeVar("_T")
2 | from typing import TypeVar
3 |
4 | _T = typing.TypeVar("_T")
| ^^ PYI018
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
5 | _P = TypeVar("_P")
|
PYI018.py:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
PYI018.py:5:1: PYI018 Private TypeVar `_P` is never used
|
6 | _T = typing.TypeVar("_T")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
| ^^^ PYI018
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
4 | _T = typing.TypeVar("_T")
5 | _P = TypeVar("_P")
| ^^ PYI018
6 |
7 | # OK
|
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
|

View File

@@ -1,52 +1,22 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI018.pyi:6:1: PYI018 Private TypeVar `_T` is never used
PYI018.pyi:4:1: PYI018 Private TypeVar `_T` is never used
|
4 | from typing_extensions import ParamSpec, TypeVarTuple
5 |
6 | _T = typing.TypeVar("_T")
2 | from typing import TypeVar
3 |
4 | _T = typing.TypeVar("_T")
| ^^ PYI018
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
5 | _P = TypeVar("_P")
|
PYI018.pyi:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
PYI018.pyi:5:1: PYI018 Private TypeVar `_P` is never used
|
6 | _T = typing.TypeVar("_T")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
| ^^^ PYI018
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
4 | _T = typing.TypeVar("_T")
5 | _P = TypeVar("_P")
| ^^ PYI018
6 |
7 | # OK
|
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
|

View File

@@ -54,91 +54,5 @@ 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 | ...

View File

@@ -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] | str) -> None: ...
10 |+def func(arg: type[int | float]) -> None: ...
11 11 |
12 12 | # OK
13 13 | x: type[int, str, float]

View File

@@ -56,7 +56,6 @@ 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<()> {

View File

@@ -1,5 +1,3 @@
use std::cmp;
use anyhow::Result;
use libcst_native::CompOp;
@@ -16,7 +14,6 @@ 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
@@ -81,65 +78,18 @@ impl Violation for YodaConditions {
}
}
/// 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
}
/// 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(),
}
}
@@ -230,9 +180,7 @@ pub(crate) fn yoda_conditions(
return;
}
if ConstantLikelihood::from_expression(left, checker.settings.preview)
<= ConstantLikelihood::from_expression(right, checker.settings.preview)
{
if !is_constant_like(left) || is_constant_like(right) {
return;
}

View File

@@ -6,8 +6,8 @@ SIM300.py:2:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda
1 | # Errors
2 | "yoda" == compare # SIM300
| ^^^^^^^^^^^^^^^^^ SIM300
3 | 42 == age # SIM300
4 | ("a", "b") == compare # SIM300
3 | "yoda" == compare # SIM300
4 | 42 == age # SIM300
|
= help: Replace Yoda condition with `compare == "yoda"`
@@ -15,340 +15,342 @@ 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 | 42 == age # SIM300
4 4 | ("a", "b") == compare # SIM300
5 5 | "yoda" <= compare # SIM300
3 3 | "yoda" == compare # SIM300
4 4 | 42 == age # SIM300
5 5 | ("a", "b") == compare # SIM300
SIM300.py:3:1: SIM300 [*] Yoda conditions are discouraged, use `age == 42` instead
SIM300.py:3:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda"` instead
|
1 | # Errors
2 | "yoda" == compare # SIM300
3 | 42 == age # 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
| ^^^^^^^^^ SIM300
4 | ("a", "b") == compare # SIM300
5 | "yoda" <= compare # SIM300
5 | ("a", "b") == compare # SIM300
6 | "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
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
SIM300.py:4:1: SIM300 [*] Yoda conditions are discouraged, use `compare == ("a", "b")` instead
SIM300.py:5:1: SIM300 [*] Yoda conditions are discouraged, use `compare == ("a", "b")` instead
|
2 | "yoda" == compare # SIM300
3 | 42 == age # SIM300
4 | ("a", "b") == compare # SIM300
3 | "yoda" == compare # SIM300
4 | 42 == age # SIM300
5 | ("a", "b") == compare # SIM300
| ^^^^^^^^^^^^^^^^^^^^^ SIM300
5 | "yoda" <= compare # SIM300
6 | "yoda" < compare # SIM300
6 | "yoda" <= compare # SIM300
7 | "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
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
SIM300.py:5:1: SIM300 [*] Yoda conditions are discouraged, use `compare >= "yoda"` instead
SIM300.py:6:1: SIM300 [*] Yoda conditions are discouraged, use `compare >= "yoda"` instead
|
3 | 42 == age # SIM300
4 | ("a", "b") == compare # SIM300
5 | "yoda" <= compare # SIM300
4 | 42 == age # SIM300
5 | ("a", "b") == compare # SIM300
6 | "yoda" <= compare # SIM300
| ^^^^^^^^^^^^^^^^^ SIM300
6 | "yoda" < compare # SIM300
7 | 42 > age # SIM300
7 | "yoda" < compare # SIM300
8 | 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
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
SIM300.py:6:1: SIM300 [*] Yoda conditions are discouraged, use `compare > "yoda"` instead
SIM300.py:7:1: SIM300 [*] Yoda conditions are discouraged, use `compare > "yoda"` instead
|
4 | ("a", "b") == compare # SIM300
5 | "yoda" <= compare # SIM300
6 | "yoda" < compare # SIM300
5 | ("a", "b") == compare # SIM300
6 | "yoda" <= compare # SIM300
7 | "yoda" < compare # SIM300
| ^^^^^^^^^^^^^^^^ SIM300
7 | 42 > age # SIM300
8 | -42 > age # SIM300
8 | 42 > age # SIM300
9 | -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
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
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`
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`
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
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
SIM300.py:8:1: SIM300 [*] Yoda conditions are discouraged, use `age < -42` instead
SIM300.py:9:1: SIM300 [*] Yoda conditions are discouraged, use `age < -42` instead
|
6 | "yoda" < compare # SIM300
7 | 42 > age # SIM300
8 | -42 > age # SIM300
7 | "yoda" < compare # SIM300
8 | 42 > age # SIM300
9 | -42 > age # SIM300
| ^^^^^^^^^ SIM300
9 | +42 > age # SIM300
10 | YODA == age # SIM300
10 | +42 > age # SIM300
11 | 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
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
SIM300.py:9:1: SIM300 [*] Yoda conditions are discouraged, use `age < +42` instead
SIM300.py:10:1: SIM300 [*] Yoda conditions are discouraged, use `age < +42` instead
|
7 | 42 > age # SIM300
8 | -42 > age # SIM300
9 | +42 > age # SIM300
8 | 42 > age # SIM300
9 | -42 > age # SIM300
10 | +42 > age # SIM300
| ^^^^^^^^^ SIM300
10 | YODA == age # SIM300
11 | YODA > age # SIM300
11 | YODA == age # SIM300
12 | 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
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
SIM300.py:10:1: SIM300 [*] Yoda conditions are discouraged, use `age == YODA` instead
SIM300.py:11:1: SIM300 [*] Yoda conditions are discouraged, use `age == YODA` instead
|
8 | -42 > age # SIM300
9 | +42 > age # SIM300
10 | YODA == age # SIM300
9 | -42 > age # SIM300
10 | +42 > age # SIM300
11 | YODA == age # SIM300
| ^^^^^^^^^^^ SIM300
11 | YODA > age # SIM300
12 | YODA >= age # SIM300
12 | YODA > age # SIM300
13 | 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
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
SIM300.py:11:1: SIM300 [*] Yoda conditions are discouraged, use `age < YODA` instead
SIM300.py:12:1: SIM300 [*] Yoda conditions are discouraged, use `age < YODA` instead
|
9 | +42 > age # SIM300
10 | YODA == age # SIM300
11 | YODA > age # SIM300
10 | +42 > age # SIM300
11 | YODA == age # SIM300
12 | YODA > age # SIM300
| ^^^^^^^^^^ SIM300
12 | YODA >= age # SIM300
13 | JediOrder.YODA == age # SIM300
13 | YODA >= age # SIM300
14 | 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
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
SIM300.py:12:1: SIM300 [*] Yoda conditions are discouraged, use `age <= YODA` instead
SIM300.py:13:1: SIM300 [*] Yoda conditions are discouraged, use `age <= YODA` instead
|
10 | YODA == age # SIM300
11 | YODA > age # SIM300
12 | YODA >= age # SIM300
11 | YODA == age # SIM300
12 | YODA > age # SIM300
13 | YODA >= age # SIM300
| ^^^^^^^^^^^ SIM300
13 | JediOrder.YODA == age # SIM300
14 | 0 < (number - 100) # SIM300
14 | JediOrder.YODA == age # SIM300
15 | 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
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
SIM300.py:13:1: SIM300 [*] Yoda conditions are discouraged, use `age == JediOrder.YODA` instead
SIM300.py:14:1: SIM300 [*] Yoda conditions are discouraged, use `age == JediOrder.YODA` instead
|
11 | YODA > age # SIM300
12 | YODA >= age # SIM300
13 | JediOrder.YODA == age # SIM300
12 | YODA > age # SIM300
13 | YODA >= age # SIM300
14 | JediOrder.YODA == age # SIM300
| ^^^^^^^^^^^^^^^^^^^^^ SIM300
14 | 0 < (number - 100) # SIM300
15 | B<A[0][0]or B
15 | 0 < (number - 100) # SIM300
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
|
= 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]
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
SIM300.py:14:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100) > 0` instead
SIM300.py:15: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
13 | YODA >= age # SIM300
14 | JediOrder.YODA == age # SIM300
15 | 0 < (number - 100) # SIM300
| ^^^^^^^^^^^^^^^^^^ SIM300
15 | B<A[0][0]or B
16 | B or(B)<A[0][0]
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
17 | B<A[0][0]or B
|
= 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 |
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]
SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > B` instead
SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged
|
13 | JediOrder.YODA == age # SIM300
14 | 0 < (number - 100) # SIM300
15 | B<A[0][0]or B
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
| ^^^^^^^^^ SIM300
16 | B or(B)<A[0][0]
18 | 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
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
SIM300.py:16:5: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > (B)` instead
SIM300.py:18: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]
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
17 | B<A[0][0]or B
18 | B or(B)<A[0][0]
| ^^^^^^^^^^^ SIM300
17 |
18 | # Errors in preview
19 |
20 | # OK
|
= 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: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"
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"

View File

@@ -1,354 +0,0 @@
---
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']

View File

@@ -1,12 +1,13 @@
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;
use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_codegen::Stylist;
use ruff_python_semantic::{
analyze, Binding, BindingKind, NodeId, ResolvedReference, SemanticModel,
Binding, BindingId, BindingKind, NodeId, ResolvedReference, SemanticModel,
};
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
@@ -58,17 +59,57 @@ 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 {
analyze::class::any_over_body(class_def, semantic, &|call_path| {
base_classes
.iter()
.any(|base_class| from_qualified_name(base_class) == call_path)
})
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())
}
fn runtime_required_decorators(
@@ -174,7 +215,6 @@ 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) {
@@ -184,7 +224,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, generator);
return quote_annotation(parent_id, semantic, locator, stylist);
}
}
Some(Expr::Attribute(parent)) => {
@@ -192,7 +232,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, generator);
return quote_annotation(parent_id, semantic, locator, stylist);
}
}
Some(Expr::Call(parent)) => {
@@ -200,7 +240,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, generator);
return quote_annotation(parent_id, semantic, locator, stylist);
}
}
Some(Expr::BinOp(parent)) => {
@@ -208,44 +248,27 @@ 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, generator);
return quote_annotation(parent_id, semantic, locator, stylist);
}
}
_ => {}
}
}
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"]]
// ```
let text = locator.slice(expr);
if text.contains('\'') || text.contains('"') {
if annotation.contains('\'') || annotation.contains('"') {
return Err(anyhow::anyhow!("Annotation already contains a quote"));
}
// Quote the entire expression.
// If we're quoting a name, we need to quote the entire expression.
let quote = stylist.quote();
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
let annotation = format!("{quote}{annotation}{quote}");
Ok(Edit::range_replacement(annotation, expr.range()))
}

View File

@@ -35,8 +35,6 @@ 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"))]
@@ -106,35 +104,6 @@ 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")

View File

@@ -1,9 +1,7 @@
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;

View File

@@ -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::{filter_contained, quote_annotation};
use crate::rules::flake8_type_checking::helpers::quote_annotation;
use crate::rules::flake8_type_checking::imports::ImportBinding;
/// ## What it does
@@ -262,33 +262,32 @@ 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 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
}
})
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
}
})
.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),
)))
})
.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),
)),
)
}
/// Generate a [`Fix`] to remove runtime imports from a type-checking block.

View File

@@ -1,95 +0,0 @@
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);
}
_ => {}
}
}

View File

@@ -12,9 +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::{
filter_contained, is_typing_reference, quote_annotation,
};
use crate::rules::flake8_type_checking::helpers::{is_typing_reference, quote_annotation};
use crate::rules::flake8_type_checking::imports::ImportBinding;
use crate::rules::isort::{categorize, ImportSection, ImportType};
@@ -473,47 +471,41 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
)?;
// Step 2) Add the import to a `TYPE_CHECKING` block.
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();
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,
)?;
// Step 3) Quote any runtime usages of the referenced symbol.
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
}
})
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
}
})
.collect::<Result<Vec<_>>>()?,
);
})
.collect::<Result<Vec<_>>>()?;
Ok(Fix::unsafe_edits(
type_checking_edit,
remove_import_edit,
add_import_edit
.into_edits()
.into_iter()
.chain(std::iter::once(remove_import_edit))
.chain(quote_reference_edits),
)
.isolate(Checker::isolation(

View File

@@ -18,7 +18,5 @@ 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 |

View File

@@ -196,146 +196,4 @@ 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 | ...

View File

@@ -1,12 +0,0 @@
---
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
|

View File

@@ -1,41 +0,0 @@
---
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
|

View File

@@ -1,27 +0,0 @@
---
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] = {}

View File

@@ -1,27 +0,0 @@
---
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] = {}

View File

@@ -1,28 +0,0 @@
---
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] = {}

View File

@@ -51,19 +51,6 @@ 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`

View File

@@ -45,24 +45,11 @@ 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]

View File

@@ -254,26 +254,14 @@ 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())
.after(line.start() + indent.text_len())
.chars()
.take(docstring.indentation.chars().count() + over_indented_size)
.take(over_indented_size)
.map(TextLen::text_len)
.sum::<TextSize>();
let range = TextRange::at(line.start(), offset);
let range = TextRange::at(line.start(), indent.text_len() + offset);
Edit::range_replacement(indent, range)
};
diagnostic.set_fix(Fix::safe_edit(edit));

View File

@@ -411,22 +411,4 @@ 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 | """

View File

@@ -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,27 +679,4 @@ 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 | """

View File

@@ -52,7 +52,6 @@ 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"))]

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -346,22 +346,6 @@ 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(

View File

@@ -55,7 +55,6 @@ 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::*;
@@ -133,7 +132,6 @@ 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;

View File

@@ -1,59 +0,0 @@
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(),
));
};
}
}

View File

@@ -45,7 +45,6 @@ pub struct Settings {
pub max_branches: usize,
pub max_statements: usize,
pub max_public_methods: usize,
pub max_locals: usize,
}
impl Default for Settings {
@@ -60,7 +59,6 @@ impl Default for Settings {
max_branches: 12,
max_statements: 50,
max_public_methods: 20,
max_locals: 15,
}
}
}

View File

@@ -1,12 +0,0 @@
---
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
|

View File

@@ -16,7 +16,6 @@ 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"))]

View File

@@ -8,7 +8,6 @@ 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::*;
@@ -26,7 +25,6 @@ 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;

View File

@@ -1,319 +0,0 @@
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
}
}

View File

@@ -1,701 +0,0 @@
---
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`

View File

@@ -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, Scope, SemanticModel};
use ruff_python_semantic::{analyze::typing, Binding, SemanticModel};
use ruff_text_size::Ranged;
/// ## What it does
@@ -105,50 +105,22 @@ pub(crate) fn asyncio_dangling_task(expr: &Expr, semantic: &SemanticModel) -> Op
/// RUF006
pub(crate) fn asyncio_dangling_binding(
scope: &Scope,
binding: &Binding,
semantic: &SemanticModel,
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);
}
) -> 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)
}
Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
}) => asyncio_dangling_task(value, semantic),
_ => None,
}
}

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::{analyze, BindingKind, SemanticModel};
use ruff_python_semantic::{BindingKind, SemanticModel};
/// Return `true` if the given [`Expr`] is a special class attribute, like `__slots__`.
///
@@ -56,13 +57,19 @@ pub(super) fn has_default_copy_semantics(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
) -> bool {
analyze::class::any_over_body(class_def, semantic, &|call_path| {
matches!(
call_path.as_slice(),
["pydantic", "BaseModel" | "BaseSettings"]
| ["pydantic_settings", "BaseSettings"]
| ["msgspec", "Struct"]
)
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"]
)
})
})
}

View File

@@ -1,19 +0,0 @@
{
"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 = (",
" \"Ты — Сайга, русскоязычный автоматический ассистент. \"",
" \"Ты разговариваешь с людьми и помогаешь им.\"",
")"
]
}

View File

@@ -171,43 +171,48 @@ impl Cell {
// Detect cell magics (which operate on multiple lines).
lines.any(|line| {
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"
)
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"
)
})
})
}
}

View File

@@ -421,18 +421,17 @@ mod tests {
));
}
#[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<()> {
#[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<()> {
/// 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);
@@ -440,10 +439,7 @@ mod tests {
Ok(serde_json::from_str(&source_code)?)
}
assert_eq!(
read_jupyter_cell(format!("{cell}.json"))?.is_valid_code_cell(),
expected
);
assert_eq!(read_jupyter_cell(path)?.is_valid_code_cell(), expected);
Ok(())
}

View File

@@ -921,204 +921,178 @@ where
}
}
#[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 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
}
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;
}
/// 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;
}
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if returns(body).is_some() {
return false;
}
if sometimes_breaks(orelse) {
return true;
}
if returns(body) {
return true;
}
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
if std::iter::once(body)
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)
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
.any(|body| sometimes_breaks(body))
{
return true;
}
.all(|body| returns(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
}
/// 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,
_ => {}
Stmt::Match(ast::StmtMatch { cases, .. }) => {
// Note: we assume the `match` is exhaustive.
if cases.iter().all(|case| returns(&case.body)) {
return true;
}
}
}
false
}
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
// If the `finally` block returns, the `try` block must also return.
if returns(finalbody) {
return true;
}
/// 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);
}
}
// 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::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),
_ => {}
}
Stmt::With(ast::StmtWith { body, .. }) => {
if returns(body) {
return true;
}
}
Stmt::Return(_) => return true,
Stmt::Raise(_) => return true,
_ => {}
}
None
}
returns(&function.body)
false
}
/// 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()
}
!returns(&function.body)
}
/// A [`StatementVisitor`] that collects all `raise` statements in a function or method.

View File

@@ -33,15 +33,9 @@ node_lines = (
nodes = []
for node_line in node_lines:
node = node_line.split("(")[1].split(")")[0].split("::")[-1].split("<")[0]
# `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",
):
# 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"):
continue
nodes.append(node)
print(nodes)

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