Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7755d7a8d | ||
|
|
11449acfd9 | ||
|
|
4ccbacd44b | ||
|
|
05a2f52206 | ||
|
|
a6f7100b55 | ||
|
|
50122d2308 | ||
|
|
ad2cfa3dba | ||
|
|
0045032905 | ||
|
|
bea8f2ee3a | ||
|
|
c8074b0e18 | ||
|
|
740d8538de | ||
|
|
341d7447a0 | ||
|
|
2e8aff647b | ||
|
|
e555cf0ae4 | ||
|
|
b9139a31d5 | ||
|
|
7329bf459c | ||
|
|
157d5bacfc | ||
|
|
d496c164d3 | ||
|
|
b61b0edeea | ||
|
|
79a0ddc112 | ||
|
|
91046e4c81 | ||
|
|
5fe0fdd0a8 | ||
|
|
ffd13e65ae | ||
|
|
dba2cb79cb | ||
|
|
45628a5883 | ||
|
|
fc3e2664f9 | ||
|
|
b0d6fd7343 | ||
|
|
87821252d7 | ||
|
|
57313d9d63 | ||
|
|
eab1a6862b | ||
|
|
395bf3dc98 | ||
|
|
47b8a897e7 | ||
|
|
e81976f754 | ||
|
|
f5061dbb8e | ||
|
|
a3d667ed04 | ||
|
|
76a069d0c9 | ||
|
|
b994fb3ce0 | ||
|
|
40a0231189 | ||
|
|
9c8a4d927e | ||
|
|
836e4406a5 | ||
|
|
e54ed28ba9 | ||
|
|
1e4b421a00 | ||
|
|
a1f3cda190 | ||
|
|
837984168a | ||
|
|
b64aa1e86d | ||
|
|
9e5f3f1b1b | ||
|
|
9dc59cbb81 | ||
|
|
a42600e9a2 | ||
|
|
49a445a23d |
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,5 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.15
|
||||
|
||||
### Preview features
|
||||
|
||||
- Error when `NURSERY` selector is used with `--preview` ([#9682](https://github.com/astral-sh/ruff/pull/9682))
|
||||
- Preserve indentation around multiline strings in formatter ([#9637](https://github.com/astral-sh/ruff/pull/9637))
|
||||
- \[`flake8-return`\] Add fixes for all rules (`RET505`, `RET506`, `RET507`, `RET508`) ([#9595](https://github.com/astral-sh/ruff/pull/9595))
|
||||
- \[`flake8-simplify`\] Add fix for `if-with-same-arms` (`SIM114`) ([#9591](https://github.com/astral-sh/ruff/pull/9591))
|
||||
- \[`pycodestyle`\] Add fix for `multiple-imports-on-one-line` (`E401`) ([#9518](https://github.com/astral-sh/ruff/pull/9518))
|
||||
- \[`pylint`\] Add fix for `collapsible-else-if` (`PLR5501`) ([#9594](https://github.com/astral-sh/ruff/pull/9594))
|
||||
- \[`pylint`\] Add fix for `useless-else-on-loop` (`PLW0120`) ([#9590](https://github.com/astral-sh/ruff/pull/9590))
|
||||
- \[`pylint`\] Implement `assigning-non-slot` (`E0237`) ([#9623](https://github.com/astral-sh/ruff/pull/9623))
|
||||
- \[`pylint`\] Implement `potential-index-error` (`PLE0643`) ([#9545](https://github.com/astral-sh/ruff/pull/9545))
|
||||
- \[`pylint`\] Implement `too-many-nested-blocks` (`PLR1702`) ([#9172](https://github.com/astral-sh/ruff/pull/9172))
|
||||
- \[`ruff`\] Add rule to sort `__slots__` and `__match_args__` ([#9564](https://github.com/astral-sh/ruff/pull/9564))
|
||||
- \[`ruff`\] Detect unnecessary `dict` comprehensions for iterables (`RUF025`) ([#9613](https://github.com/astral-sh/ruff/pull/9613))
|
||||
- \[`ruff`\] Guard against use of `default_factory` as a keyword argument (`RUF026`) ([#9651](https://github.com/astral-sh/ruff/pull/9651))
|
||||
- \[`ruff`\] Implement `mutable-fromkeys-value` (`RUF024`) ([#9597](https://github.com/astral-sh/ruff/pull/9597))
|
||||
|
||||
### CLI
|
||||
|
||||
- Enable auto-wrapping of `--help` output ([#9633](https://github.com/astral-sh/ruff/pull/9633))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid rendering display-only rules as fixable ([#9649](https://github.com/astral-sh/ruff/pull/9649))
|
||||
- Detect automagic-like assignments in notebooks ([#9653](https://github.com/astral-sh/ruff/pull/9653))
|
||||
- Generate custom JSON schema for dynamic setting ([#9632](https://github.com/astral-sh/ruff/pull/9632))
|
||||
- \[`flake8-no-pep420`\] Include global `--config` when determining namespace packages ([#9603](https://github.com/astral-sh/ruff/pull/9603))
|
||||
- \[`flake8-pie`\] Omit bound tuples passed to `.startswith` or `.endswith` ([#9661](https://github.com/astral-sh/ruff/pull/9661))
|
||||
- \[`flake8-return`\] Avoid panic when fixing inlined else blocks ([#9657](https://github.com/astral-sh/ruff/pull/9657))
|
||||
- \[`flake8-return`\] Consider exception suppression in unnecessary assignment ([#9673](https://github.com/astral-sh/ruff/pull/9673))
|
||||
- \[`flake8-return`\] Take `NoReturn` annotation into account when analyzing implicit returns ([#9636](https://github.com/astral-sh/ruff/pull/9636))
|
||||
- \[`flake8-simplify`\] Support inverted returns in `needless-bool` (`SIM103`) ([#9619](https://github.com/astral-sh/ruff/pull/9619))
|
||||
- \[`flake8-type-checking`\] Add Pydantic's `BaseConfig` to default-copy list ([#9650](https://github.com/astral-sh/ruff/pull/9650))
|
||||
- \[`flake8-type-checking`\] Avoid marking `InitVar` as a typing-only annotation ([#9688](https://github.com/astral-sh/ruff/pull/9688))
|
||||
- \[`pycodestyle`\] Allow `dtype` comparisons in `type-comparison` ([#9676](https://github.com/astral-sh/ruff/pull/9676))
|
||||
- \[`pydocstyle`\] Re-implement `last-line-after-section` (`D413`) ([#9654](https://github.com/astral-sh/ruff/pull/9654))
|
||||
|
||||
### Documentation
|
||||
|
||||
- \[`flake8-pytest-style`\] Add fix safety documentation for `duplicate-parameterize-test-cases` ([#9678](https://github.com/astral-sh/ruff/pull/9678))
|
||||
- \[`pylint`\] Document `literal-membership` fix safety conditions ([#9677](https://github.com/astral-sh/ruff/pull/9677))
|
||||
- \[`isort`\] Fix reference to `isort` rule code ([#9598](https://github.com/astral-sh/ruff/pull/9598))
|
||||
|
||||
### Other changes
|
||||
|
||||
## 0.1.14
|
||||
|
||||
### Preview features
|
||||
|
||||
67
Cargo.lock
generated
67
Cargo.lock
generated
@@ -273,14 +273,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.31"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
||||
checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -330,6 +330,7 @@ dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"terminal_size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -956,9 +957,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.21"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060"
|
||||
checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
@@ -1855,9 +1856,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.8.0"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
|
||||
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
@@ -1865,9 +1866,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.0"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
@@ -2004,7 +2005,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2164,7 +2165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2416,7 +2417,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2683,9 +2684,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.109"
|
||||
version = "1.0.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9"
|
||||
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -2712,9 +2713,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.4.0"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23"
|
||||
checksum = "f5c9fdb6b00a489875b22efd4b78fe2b363b72265cc5f6eb2e2b9ee270e6140c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_with_macros",
|
||||
@@ -2722,9 +2723,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.4.0"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788"
|
||||
checksum = "dbff351eb4b33600a2e138dfa0b10b65a238ea8ff8fb2387c422c5022a3e8298"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
@@ -2752,9 +2753,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
@@ -2770,9 +2771,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.11.2"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
|
||||
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
@@ -2891,6 +2892,16 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminfo"
|
||||
version = "0.8.0"
|
||||
@@ -3417,9 +3428,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.39"
|
||||
version = "0.4.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12"
|
||||
checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -3458,9 +3469,9 @@ checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.39"
|
||||
version = "0.3.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403"
|
||||
checksum = "139bd73305d50e1c1c4333210c0db43d989395b64a237bd35c10ef3832a7f70c"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
@@ -3472,9 +3483,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.39"
|
||||
version = "0.3.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89"
|
||||
checksum = "70072aebfe5da66d2716002c729a14e4aec4da0e23cc2ea66323dac541c93928"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -20,7 +20,7 @@ assert_cmd = { version = "2.0.13" }
|
||||
bincode = { version = "1.3.3" }
|
||||
bitflags = { version = "2.4.1" }
|
||||
cachedir = { version = "0.3.1" }
|
||||
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
|
||||
chrono = { version = "0.4.33", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.4.13", features = ["derive"] }
|
||||
clap_complete_command = { version = "0.5.1" }
|
||||
clearscreen = { version = "2.0.0" }
|
||||
@@ -40,7 +40,7 @@ fs-err = { version ="2.11.0"}
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
hexf-parse = { version ="0.2.1"}
|
||||
ignore = { version = "0.4.21" }
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff ={ version = "0.1.5"}
|
||||
imperative = { version = "1.0.4" }
|
||||
indicatif ={ version = "0.17.7"}
|
||||
@@ -69,7 +69,7 @@ pyproject-toml = { version = "0.8.1" }
|
||||
quick-junit = { version = "0.3.5" }
|
||||
quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.8.0" }
|
||||
rayon = { version = "1.8.1" }
|
||||
regex = { version = "1.10.2" }
|
||||
result-like = { version = "0.5.0" }
|
||||
rustc-hash = { version = "1.1.0" }
|
||||
@@ -78,13 +78,13 @@ seahash = { version ="4.1.0"}
|
||||
semver = { version = "1.0.21" }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde-wasm-bindgen = { version = "0.6.3" }
|
||||
serde_json = { version = "1.0.109" }
|
||||
serde_json = { version = "1.0.113" }
|
||||
serde_test = { version = "1.0.152" }
|
||||
serde_with = { version = "3.4.0", default-features = false, features = ["macros"] }
|
||||
serde_with = { version = "3.5.1", default-features = false, features = ["macros"] }
|
||||
shellexpand = { version = "3.0.0" }
|
||||
shlex = { version ="1.2.0"}
|
||||
shlex = { version ="1.3.0"}
|
||||
similar = { version = "2.4.0", features = ["inline"] }
|
||||
smallvec = { version = "1.11.2" }
|
||||
smallvec = { version = "1.13.1" }
|
||||
static_assertions = "1.1.0"
|
||||
strum = { version = "0.25.0", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.25.3" }
|
||||
@@ -107,7 +107,7 @@ url = { version = "2.5.0" }
|
||||
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
|
||||
walkdir = { version = "2.3.2" }
|
||||
wasm-bindgen = { version = "0.2.84" }
|
||||
wasm-bindgen-test = { version = "0.3.39" }
|
||||
wasm-bindgen-test = { version = "0.3.40" }
|
||||
wild = { version = "2" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
@@ -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.14
|
||||
rev: v0.1.15
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -30,7 +30,7 @@ bincode = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
cachedir = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
clap = { workspace = true, features = ["derive", "env", "wrap_help"] }
|
||||
clap_complete_command = { workspace = true }
|
||||
clearscreen = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
|
||||
@@ -878,15 +878,12 @@ fn nursery_group_selector_preview_enabled() {
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
-:1:1: CPY001 Missing copyright notice at top of file
|
||||
-:1:2: E225 [*] Missing whitespace around operator
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `NURSERY` selector has been deprecated.
|
||||
ruff failed
|
||||
Cause: The `NURSERY` selector is deprecated and cannot be used with preview mode enabled.
|
||||
"###);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ ruff_python_trivia = { path = "../ruff_python_trivia" }
|
||||
ruff_workspace = { path = "../ruff_workspace", features = ["schemars"]}
|
||||
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help"] }
|
||||
ignore = { workspace = true }
|
||||
imara-diff = { workspace = true }
|
||||
indicatif = { workspace = true }
|
||||
|
||||
@@ -114,12 +114,15 @@ pub(super) fn main(args: &Args) -> Result<()> {
|
||||
|
||||
/// Returns the output of `ruff help`.
|
||||
fn help_text() -> String {
|
||||
args::Args::command().render_help().to_string()
|
||||
args::Args::command()
|
||||
.term_width(79)
|
||||
.render_help()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Returns the output of a given subcommand (e.g., `ruff help check`).
|
||||
fn subcommand_help_text(subcommand: &str) -> Result<String> {
|
||||
let mut cmd = args::Args::command();
|
||||
let mut cmd = args::Args::command().term_width(79);
|
||||
|
||||
// The build call is necessary for the help output to contain `Usage: ruff
|
||||
// check` instead of `Usage: check` see https://github.com/clap-rs/clap/issues/4685
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -9,6 +9,22 @@ obj.startswith(foo) or obj.startswith("foo")
|
||||
# error
|
||||
obj.endswith(foo) or obj.startswith(foo) or obj.startswith("foo")
|
||||
|
||||
def func():
|
||||
msg = "hello world"
|
||||
|
||||
x = "h"
|
||||
y = ("h", "e", "l", "l", "o")
|
||||
z = "w"
|
||||
|
||||
if msg.startswith(x) or msg.startswith(y) or msg.startswith(z): # Error
|
||||
print("yes")
|
||||
|
||||
def func():
|
||||
msg = "hello world"
|
||||
|
||||
if msg.startswith(("h", "e", "l", "l", "o")) or msg.startswith("h") or msg.startswith("w"): # Error
|
||||
print("yes")
|
||||
|
||||
# ok
|
||||
obj.startswith(("foo", "bar"))
|
||||
# ok
|
||||
@@ -17,3 +33,46 @@ obj.endswith(("foo", "bar"))
|
||||
obj.startswith("foo") or obj.endswith("bar")
|
||||
# ok
|
||||
obj.startswith("foo") or abc.startswith("bar")
|
||||
|
||||
def func():
|
||||
msg = "hello world"
|
||||
|
||||
x = "h"
|
||||
y = ("h", "e", "l", "l", "o")
|
||||
|
||||
if msg.startswith(x) or msg.startswith(y): # OK
|
||||
print("yes")
|
||||
|
||||
def func():
|
||||
msg = "hello world"
|
||||
|
||||
y = ("h", "e", "l", "l", "o")
|
||||
|
||||
if msg.startswith(y): # OK
|
||||
print("yes")
|
||||
|
||||
def func():
|
||||
msg = "hello world"
|
||||
|
||||
y = ("h", "e", "l", "l", "o")
|
||||
|
||||
if msg.startswith(y) or msg.startswith(y): # OK
|
||||
print("yes")
|
||||
|
||||
def func():
|
||||
msg = "hello world"
|
||||
|
||||
y = ("h", "e", "l", "l", "o")
|
||||
x = ("w", "o", "r", "l", "d")
|
||||
|
||||
if msg.startswith(y) or msg.startswith(x) or msg.startswith("h"): # OK
|
||||
print("yes")
|
||||
|
||||
def func():
|
||||
msg = "hello world"
|
||||
|
||||
y = ("h", "e", "l", "l", "o")
|
||||
x = ("w", "o", "r", "l", "d")
|
||||
|
||||
if msg.startswith(y) or msg.endswith(x) or msg.startswith("h"): # OK
|
||||
print("yes")
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import _thread
|
||||
import builtins
|
||||
import os
|
||||
import posix
|
||||
from posix import abort
|
||||
import sys as std_sys
|
||||
import typing
|
||||
import typing_extensions
|
||||
import _thread
|
||||
import _winapi
|
||||
from posix import abort
|
||||
from typing import NoReturn
|
||||
|
||||
import _winapi
|
||||
import pytest
|
||||
import typing_extensions
|
||||
from pytest import xfail as py_xfail
|
||||
|
||||
###
|
||||
@@ -326,3 +327,44 @@ def end_of_file():
|
||||
if False:
|
||||
return 1
|
||||
x = 2 \
|
||||
|
||||
|
||||
|
||||
# function return type annotation NoReturn
|
||||
def foo(x: int) -> int:
|
||||
def bar() -> NoReturn:
|
||||
abort()
|
||||
if x == 5:
|
||||
return 5
|
||||
bar()
|
||||
|
||||
|
||||
def foo(string: str) -> str:
|
||||
def raises(value: str) -> NoReturn:
|
||||
raise RuntimeError("something went wrong")
|
||||
|
||||
match string:
|
||||
case "a":
|
||||
return "first"
|
||||
case "b":
|
||||
return "second"
|
||||
case "c":
|
||||
return "third"
|
||||
case _:
|
||||
raises(string)
|
||||
|
||||
|
||||
def foo() -> int:
|
||||
def baz() -> int:
|
||||
return 1
|
||||
|
||||
|
||||
def bar() -> NoReturn:
|
||||
a = 1 + 2
|
||||
raise AssertionError("Very bad")
|
||||
|
||||
|
||||
|
||||
if baz() > 3:
|
||||
return 1
|
||||
bar()
|
||||
|
||||
@@ -363,3 +363,46 @@ def foo():
|
||||
def mavko_debari(P_kbar):
|
||||
D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
|
||||
return D
|
||||
|
||||
|
||||
# contextlib suppress in with statement
|
||||
import contextlib
|
||||
|
||||
|
||||
def foo():
|
||||
x = 2
|
||||
with contextlib.suppress(Exception):
|
||||
x = x + 1
|
||||
return x
|
||||
|
||||
|
||||
def foo(data):
|
||||
with open("in.txt") as file_out, contextlib.suppress(IOError):
|
||||
file_out.write(data)
|
||||
data = 10
|
||||
return data
|
||||
|
||||
|
||||
def foo(data):
|
||||
with open("in.txt") as file_out:
|
||||
file_out.write(data)
|
||||
with contextlib.suppress(IOError):
|
||||
data = 10
|
||||
return data
|
||||
|
||||
|
||||
def foo():
|
||||
y = 1
|
||||
x = 2
|
||||
with contextlib.suppress(Exception):
|
||||
x = 1
|
||||
y = y + 2
|
||||
return y # RET504
|
||||
|
||||
|
||||
def foo():
|
||||
y = 1
|
||||
if y > 0:
|
||||
with contextlib.suppress(Exception):
|
||||
y = 2
|
||||
return y
|
||||
|
||||
@@ -131,6 +131,51 @@ def bar3(x, y, z):
|
||||
return None
|
||||
|
||||
|
||||
def bar4(x):
|
||||
if True:
|
||||
return
|
||||
else:
|
||||
# comment
|
||||
pass
|
||||
|
||||
|
||||
def bar5():
|
||||
if True:
|
||||
return
|
||||
else: # comment
|
||||
pass
|
||||
|
||||
|
||||
def bar6():
|
||||
if True:
|
||||
return
|
||||
else\
|
||||
:\
|
||||
# comment
|
||||
pass
|
||||
|
||||
|
||||
def bar7():
|
||||
if True:
|
||||
return
|
||||
else\
|
||||
: # comment
|
||||
pass
|
||||
|
||||
|
||||
def bar8():
|
||||
if True:
|
||||
return
|
||||
else: pass
|
||||
|
||||
|
||||
def bar9():
|
||||
if True:
|
||||
return
|
||||
else:\
|
||||
pass
|
||||
|
||||
|
||||
x = 0
|
||||
|
||||
if x == 1:
|
||||
|
||||
@@ -4,6 +4,11 @@ if a:
|
||||
elif c:
|
||||
b
|
||||
|
||||
if a: # we preserve comments, too!
|
||||
b
|
||||
elif c: # but not on the second branch
|
||||
b
|
||||
|
||||
if x == 1:
|
||||
for _ in range(20):
|
||||
print("hello")
|
||||
@@ -63,6 +68,10 @@ elif result.eofs == "F":
|
||||
errors = 1
|
||||
elif result.eofs == "E":
|
||||
errors = 1
|
||||
elif result.eofs == "X":
|
||||
errors = 1
|
||||
elif result.eofs == "C":
|
||||
errors = 1
|
||||
|
||||
|
||||
# OK
|
||||
@@ -70,7 +79,7 @@ def complicated_calc(*arg, **kwargs):
|
||||
return 42
|
||||
|
||||
|
||||
def foo(p):
|
||||
def func(p):
|
||||
if p == 2:
|
||||
return complicated_calc(microsecond=0)
|
||||
elif p == 3:
|
||||
@@ -103,7 +112,7 @@ elif c:
|
||||
x = 1
|
||||
|
||||
|
||||
def foo():
|
||||
def func():
|
||||
a = True
|
||||
b = False
|
||||
if a > b: # end-of-line
|
||||
@@ -114,3 +123,23 @@ def foo():
|
||||
return 4
|
||||
elif b is None:
|
||||
return 4
|
||||
|
||||
|
||||
def func():
|
||||
"""Ensure that the named expression is parenthesized when merged."""
|
||||
a = True
|
||||
b = False
|
||||
if a > b: # end-of-line
|
||||
return 3
|
||||
elif a := 1:
|
||||
return 3
|
||||
|
||||
|
||||
if a: # we preserve comments, too!
|
||||
b
|
||||
elif c: # but not on the second branch
|
||||
b
|
||||
|
||||
|
||||
if a: b # here's a comment
|
||||
elif c: b
|
||||
|
||||
18
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/init_var.py
vendored
Normal file
18
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/init_var.py
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Test: avoid marking an `InitVar` as typing-only."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import FrozenInstanceError, InitVar, dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
i: int
|
||||
j: int = None
|
||||
database: InitVar[Path] = None
|
||||
|
||||
err: FrozenInstanceError = None
|
||||
|
||||
def __post_init__(self, database):
|
||||
...
|
||||
@@ -1,5 +1,6 @@
|
||||
#: E401
|
||||
import os, sys
|
||||
|
||||
#: Okay
|
||||
import os
|
||||
import sys
|
||||
@@ -59,3 +60,21 @@ import foo
|
||||
a = 1
|
||||
|
||||
import bar
|
||||
|
||||
#: E401
|
||||
import re as regex, string # also with a comment!
|
||||
import re as regex, string; x = 1
|
||||
|
||||
x = 1; import re as regex, string
|
||||
|
||||
|
||||
def blah():
|
||||
import datetime as dt, copy
|
||||
|
||||
def nested_and_tested():
|
||||
import builtins, textwrap as tw
|
||||
|
||||
x = 1; import re as regex, string
|
||||
import re as regex, string; x = 1
|
||||
|
||||
if True: import re as regex, string
|
||||
|
||||
@@ -126,3 +126,15 @@ class Foo:
|
||||
# Okay
|
||||
if type(value) is str:
|
||||
...
|
||||
|
||||
|
||||
import numpy as np
|
||||
|
||||
#: Okay
|
||||
x.dtype == float
|
||||
|
||||
#: Okay
|
||||
np.dtype(int) == float
|
||||
|
||||
#: E721
|
||||
dtype == float
|
||||
|
||||
59
crates/ruff_linter/resources/test/fixtures/pydocstyle/D413.py
vendored
Normal file
59
crates/ruff_linter/resources/test/fixtures/pydocstyle/D413.py
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Do something.
|
||||
|
||||
Args:
|
||||
x: the value
|
||||
with a hanging indent
|
||||
|
||||
Returns:
|
||||
the value
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""Do something.
|
||||
|
||||
Args:
|
||||
x: the value
|
||||
with a hanging indent
|
||||
|
||||
Returns:
|
||||
the value
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""Do something.
|
||||
|
||||
Args:
|
||||
x: the value
|
||||
with a hanging indent
|
||||
|
||||
Returns:
|
||||
the value
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""Do something.
|
||||
|
||||
Args:
|
||||
x: the value
|
||||
with a hanging indent
|
||||
|
||||
Returns:
|
||||
the value
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""Do something.
|
||||
|
||||
Args:
|
||||
x: the value
|
||||
with a hanging indent
|
||||
|
||||
Returns:
|
||||
the value"""
|
||||
@@ -49,6 +49,17 @@ def not_ok1():
|
||||
pass
|
||||
|
||||
|
||||
def not_ok1_with_comments():
|
||||
if 1:
|
||||
pass
|
||||
else:
|
||||
# inner comment
|
||||
if 2:
|
||||
pass
|
||||
else:
|
||||
pass # final pass comment
|
||||
|
||||
|
||||
# Regression test for https://github.com/apache/airflow/blob/f1e1cdcc3b2826e68ba133f350300b5065bbca33/airflow/models/dag.py#L1737
|
||||
def not_ok2():
|
||||
if True:
|
||||
@@ -61,3 +72,28 @@ def not_ok2():
|
||||
else:
|
||||
print(4)
|
||||
|
||||
|
||||
def not_ok3():
|
||||
if 1:
|
||||
pass
|
||||
else:
|
||||
if 2: pass
|
||||
else: pass
|
||||
|
||||
|
||||
def not_ok4():
|
||||
if 1:
|
||||
pass
|
||||
else:
|
||||
if 2: pass
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def not_ok5():
|
||||
if 1:
|
||||
pass
|
||||
else:
|
||||
if 2:
|
||||
pass
|
||||
else: pass
|
||||
|
||||
56
crates/ruff_linter/resources/test/fixtures/pylint/non_slot_assignment.py
vendored
Normal file
56
crates/ruff_linter/resources/test/fixtures/pylint/non_slot_assignment.py
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
class StudentA:
|
||||
__slots__ = ("name",)
|
||||
|
||||
def __init__(self, name, surname):
|
||||
self.name = name
|
||||
self.surname = surname # [assigning-non-slot]
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
|
||||
class StudentB:
|
||||
__slots__ = ("name", "surname")
|
||||
|
||||
def __init__(self, name, middle_name):
|
||||
self.name = name
|
||||
self.middle_name = middle_name # [assigning-non-slot]
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
|
||||
class StudentC:
|
||||
__slots__ = ("name", "surname")
|
||||
|
||||
def __init__(self, name, surname):
|
||||
self.name = name
|
||||
self.surname = surname # OK
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
|
||||
class StudentD(object):
|
||||
__slots__ = ("name", "surname")
|
||||
|
||||
def __init__(self, name, middle_name):
|
||||
self.name = name
|
||||
self.middle_name = middle_name # [assigning-non-slot]
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
|
||||
class StudentE(StudentD):
|
||||
def __init__(self, name, middle_name):
|
||||
self.name = name
|
||||
self.middle_name = middle_name # OK
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
9
crates/ruff_linter/resources/test/fixtures/pylint/potential_index_error.py
vendored
Normal file
9
crates/ruff_linter/resources/test/fixtures/pylint/potential_index_error.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
print([1, 2, 3][3]) # PLE0643
|
||||
print([1, 2, 3][-4]) # PLE0643
|
||||
print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643
|
||||
print([1, 2, 3][-999999999999999999999999999999999999999999]) # PLE0643
|
||||
|
||||
print([1, 2, 3][2]) # OK
|
||||
print([1, 2, 3][0]) # OK
|
||||
print([1, 2, 3][-3]) # OK
|
||||
print([1, 2, 3][3:]) # OK
|
||||
21
crates/ruff_linter/resources/test/fixtures/pylint/too_many_nested_blocks.py
vendored
Normal file
21
crates/ruff_linter/resources/test/fixtures/pylint/too_many_nested_blocks.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
def correct_fruits(fruits) -> bool:
|
||||
if len(fruits) > 1: # PLR1702
|
||||
if "apple" in fruits:
|
||||
if "orange" in fruits:
|
||||
count = fruits["orange"]
|
||||
if count % 2:
|
||||
if "kiwi" in fruits:
|
||||
if count == 2:
|
||||
return True
|
||||
return False
|
||||
|
||||
# Ok
|
||||
def correct_fruits(fruits) -> bool:
|
||||
if len(fruits) > 1:
|
||||
if "apple" in fruits:
|
||||
if "orange" in fruits:
|
||||
count = fruits["orange"]
|
||||
if count % 2:
|
||||
if "kiwi" in fruits:
|
||||
return True
|
||||
return False
|
||||
@@ -135,3 +135,14 @@ def test_break_in_match():
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def test_retain_comment():
|
||||
"""Retain the comment within the `else` block"""
|
||||
for j in range(10):
|
||||
pass
|
||||
else:
|
||||
# [useless-else-on-loop]
|
||||
print("fat chance")
|
||||
for j in range(10):
|
||||
break
|
||||
|
||||
@@ -67,3 +67,15 @@ class G(F):
|
||||
without_annotation = []
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
final_variable: Final[list[int]] = []
|
||||
|
||||
|
||||
from pydantic import BaseConfig
|
||||
|
||||
|
||||
class H(BaseModel):
|
||||
class Config(BaseConfig):
|
||||
mutable_default: list[int] = []
|
||||
immutable_annotation: Sequence[int] = []
|
||||
without_annotation = []
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
final_variable: Final[list[int]] = []
|
||||
|
||||
@@ -306,3 +306,5 @@ __all__ = (
|
||||
__all__ = [
|
||||
"foo", (), "bar"
|
||||
]
|
||||
|
||||
__all__ = "foo", "an" "implicitly_concatenated_second_item", not_a_string_literal
|
||||
|
||||
234
crates/ruff_linter/resources/test/fixtures/ruff/RUF023.py
vendored
Normal file
234
crates/ruff_linter/resources/test/fixtures/ruff/RUF023.py
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
#########################
|
||||
# Single-line definitions
|
||||
#########################
|
||||
|
||||
class Klass:
|
||||
__slots__ = ["d", "c", "b", "a"] # a comment that is untouched
|
||||
__match_args__ = ("d", "c", "b", "a")
|
||||
|
||||
# Quoting style is retained,
|
||||
# but unnecessary parens are not
|
||||
__slots__: set = {'b', "c", ((('a')))}
|
||||
# Trailing commas are also not retained for single-line definitions
|
||||
# (but they are in multiline definitions)
|
||||
__match_args__: tuple = ("b", "c", "a",)
|
||||
|
||||
class Klass2:
|
||||
if bool():
|
||||
__slots__ = {"x": "docs for x", "m": "docs for m", "a": "docs for a"}
|
||||
else:
|
||||
__slots__ = "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens)
|
||||
|
||||
__match_args__: list[str] = ["the", "three", "little", "pigs"]
|
||||
__slots__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple")
|
||||
# we use natural sort,
|
||||
# not alphabetical sort or "isort-style" sort
|
||||
__slots__ = {"aadvark237", "aadvark10092", "aadvark174", "aadvark532"}
|
||||
|
||||
############################
|
||||
# Neat multiline definitions
|
||||
############################
|
||||
|
||||
class Klass3:
|
||||
__slots__ = (
|
||||
"d0",
|
||||
"c0", # a comment regarding 'c0'
|
||||
"b0",
|
||||
# a comment regarding 'a0':
|
||||
"a0"
|
||||
)
|
||||
__match_args__ = [
|
||||
"d",
|
||||
"c", # a comment regarding 'c'
|
||||
"b",
|
||||
# a comment regarding 'a':
|
||||
"a"
|
||||
]
|
||||
|
||||
##########################################
|
||||
# Messier multiline __all__ definitions...
|
||||
##########################################
|
||||
|
||||
class Klass4:
|
||||
# comment0
|
||||
__slots__ = ("d", "a", # comment1
|
||||
# comment2
|
||||
"f", "b",
|
||||
"strangely", # comment3
|
||||
# comment4
|
||||
"formatted",
|
||||
# comment5
|
||||
) # comment6
|
||||
# comment7
|
||||
|
||||
__match_args__ = [ # comment0
|
||||
# comment1
|
||||
# comment2
|
||||
"dx", "cx", "bx", "ax" # comment3
|
||||
# comment4
|
||||
# comment5
|
||||
# comment6
|
||||
] # comment7
|
||||
|
||||
# from cpython/Lib/pathlib/__init__.py
|
||||
class PurePath:
|
||||
__slots__ = (
|
||||
# The `_raw_paths` slot stores unnormalized string paths. This is set
|
||||
# in the `__init__()` method.
|
||||
'_raw_paths',
|
||||
|
||||
# The `_drv`, `_root` and `_tail_cached` slots store parsed and
|
||||
# normalized parts of the path. They are set when any of the `drive`,
|
||||
# `root` or `_tail` properties are accessed for the first time. The
|
||||
# three-part division corresponds to the result of
|
||||
# `os.path.splitroot()`, except that the tail is further split on path
|
||||
# separators (i.e. it is a list of strings), and that the root and
|
||||
# tail are normalized.
|
||||
'_drv', '_root', '_tail_cached',
|
||||
|
||||
# The `_str` slot stores the string representation of the path,
|
||||
# computed from the drive, root and tail when `__str__()` is called
|
||||
# for the first time. It's used to implement `_str_normcase`
|
||||
'_str',
|
||||
|
||||
# The `_str_normcase_cached` slot stores the string path with
|
||||
# normalized case. It is set when the `_str_normcase` property is
|
||||
# accessed for the first time. It's used to implement `__eq__()`
|
||||
# `__hash__()`, and `_parts_normcase`
|
||||
'_str_normcase_cached',
|
||||
|
||||
# The `_parts_normcase_cached` slot stores the case-normalized
|
||||
# string path after splitting on path separators. It's set when the
|
||||
# `_parts_normcase` property is accessed for the first time. It's used
|
||||
# to implement comparison methods like `__lt__()`.
|
||||
'_parts_normcase_cached',
|
||||
|
||||
# The `_hash` slot stores the hash of the case-normalized string
|
||||
# path. It's set when `__hash__()` is called for the first time.
|
||||
'_hash',
|
||||
)
|
||||
|
||||
# From cpython/Lib/pickletools.py
|
||||
class ArgumentDescriptor(object):
|
||||
__slots__ = (
|
||||
# name of descriptor record, also a module global name; a string
|
||||
'name',
|
||||
|
||||
# length of argument, in bytes; an int; UP_TO_NEWLINE and
|
||||
# TAKEN_FROM_ARGUMENT{1,4,8} are negative values for variable-length
|
||||
# cases
|
||||
'n',
|
||||
|
||||
# a function taking a file-like object, reading this kind of argument
|
||||
# from the object at the current position, advancing the current
|
||||
# position by n bytes, and returning the value of the argument
|
||||
'reader',
|
||||
|
||||
# human-readable docs for this arg descriptor; a string
|
||||
'doc',
|
||||
)
|
||||
|
||||
####################################
|
||||
# Should be flagged, but not fixed
|
||||
####################################
|
||||
|
||||
# from cpython/Lib/test/test_inspect.py.
|
||||
# Multiline dicts are out of scope.
|
||||
class SlotUser:
|
||||
__slots__ = {'power': 'measured in kilowatts',
|
||||
'distance': 'measured in kilometers'}
|
||||
|
||||
class Klass5:
|
||||
__match_args__ = (
|
||||
"look",
|
||||
(
|
||||
"a_veeeeeeeeeeeeeeeeeeery_long_parenthesized_item"
|
||||
),
|
||||
)
|
||||
__slots__ = (
|
||||
"b",
|
||||
((
|
||||
"c"
|
||||
)),
|
||||
"a"
|
||||
)
|
||||
__slots__ = ("don't" "care" "about", "__slots__" "with", "concatenated" "strings")
|
||||
|
||||
###################################
|
||||
# These should all not get flagged:
|
||||
###################################
|
||||
|
||||
class Klass6:
|
||||
__slots__ = ()
|
||||
__match_args__ = []
|
||||
__slots__ = ("single_item",)
|
||||
__match_args__ = (
|
||||
"single_item_multiline",
|
||||
)
|
||||
__slots__ = {"single_item",}
|
||||
__slots__ = {"single_item_no_trailing_comma": "docs for that"}
|
||||
__match_args__ = [
|
||||
"single_item_multiline_no_trailing_comma"
|
||||
]
|
||||
__slots__ = ("not_a_tuple_just_a_string")
|
||||
__slots__ = ["a", "b", "c", "d"]
|
||||
__slots__ += ["e", "f", "g"]
|
||||
__slots__ = ("a", "b", "c", "d")
|
||||
|
||||
if bool():
|
||||
__slots__ += ("e", "f", "g")
|
||||
else:
|
||||
__slots__ += ["alpha", "omega"]
|
||||
|
||||
__slots__ = {"not": "sorted", "but": "includes", **a_kwarg_splat}
|
||||
|
||||
__slots__ = ("b", "a", "e", "d")
|
||||
__slots__ = ["b", "a", "e", "d"]
|
||||
__match_args__ = ["foo", "bar", "antipasti"]
|
||||
|
||||
class Klass6:
|
||||
__slots__ = (9, 8, 7)
|
||||
__match_args__ = ( # This is just an empty tuple,
|
||||
# but,
|
||||
# it's very well
|
||||
) # documented
|
||||
|
||||
# We don't deduplicate elements;
|
||||
# this just ensures that duplicate elements aren't unnecessarily
|
||||
# reordered by an autofix:
|
||||
__slots__ = (
|
||||
"duplicate_element", # comment1
|
||||
"duplicate_element", # comment3
|
||||
"duplicate_element", # comment2
|
||||
"duplicate_element", # comment0
|
||||
)
|
||||
|
||||
__slots__ = "foo", "an" "implicitly_concatenated_second_item", not_a_string_literal
|
||||
|
||||
__slots__ =[
|
||||
[]
|
||||
]
|
||||
__slots__ = [
|
||||
()
|
||||
]
|
||||
__match_args__ = (
|
||||
()
|
||||
)
|
||||
__match_args__ = (
|
||||
[]
|
||||
)
|
||||
__slots__ = (
|
||||
(),
|
||||
)
|
||||
__slots__ = (
|
||||
[],
|
||||
)
|
||||
__match_args__ = (
|
||||
"foo", [], "bar"
|
||||
)
|
||||
__match_args__ = [
|
||||
"foo", (), "bar"
|
||||
]
|
||||
|
||||
__match_args__ = {"a", "set", "for", "__match_args__", "is invalid"}
|
||||
__match_args__ = {"this": "is", "also": "invalid"}
|
||||
32
crates/ruff_linter/resources/test/fixtures/ruff/RUF024.py
vendored
Normal file
32
crates/ruff_linter/resources/test/fixtures/ruff/RUF024.py
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
pierogi_fillings = [
|
||||
"cabbage",
|
||||
"strawberry",
|
||||
"cheese",
|
||||
"blueberry",
|
||||
]
|
||||
|
||||
# Errors.
|
||||
dict.fromkeys(pierogi_fillings, [])
|
||||
dict.fromkeys(pierogi_fillings, list())
|
||||
dict.fromkeys(pierogi_fillings, {})
|
||||
dict.fromkeys(pierogi_fillings, set())
|
||||
dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||
dict.fromkeys(pierogi_fillings, dict())
|
||||
|
||||
# Okay.
|
||||
dict.fromkeys(pierogi_fillings)
|
||||
dict.fromkeys(pierogi_fillings, None)
|
||||
dict.fromkeys(pierogi_fillings, 1)
|
||||
dict.fromkeys(pierogi_fillings)
|
||||
dict.fromkeys(pierogi_fillings, ("blessed", "tuples", "don't", "mutate"))
|
||||
dict.fromkeys(pierogi_fillings, "neither do strings")
|
||||
|
||||
class MysteryBox: ...
|
||||
|
||||
dict.fromkeys(pierogi_fillings, MysteryBox)
|
||||
bar.fromkeys(pierogi_fillings, [])
|
||||
|
||||
|
||||
def bad_dict() -> None:
|
||||
dict = MysteryBox()
|
||||
dict.fromkeys(pierogi_fillings, [])
|
||||
92
crates/ruff_linter/resources/test/fixtures/ruff/RUF025.py
vendored
Normal file
92
crates/ruff_linter/resources/test/fixtures/ruff/RUF025.py
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
# Violation cases: RUF025
|
||||
|
||||
|
||||
def func():
|
||||
numbers = [1, 2, 3]
|
||||
{n: None for n in numbers} # RUF025
|
||||
|
||||
|
||||
def func():
|
||||
for key, value in {n: 1 for n in [1, 2, 3]}.items(): # RUF025
|
||||
pass
|
||||
|
||||
|
||||
def func():
|
||||
{n: 1.1 for n in [1, 2, 3]} # RUF025
|
||||
|
||||
|
||||
def func():
|
||||
{n: complex(3, 5) for n in [1, 2, 3]} # RUF025
|
||||
|
||||
|
||||
def func():
|
||||
def f(data):
|
||||
return data
|
||||
|
||||
f({c: "a" for c in "12345"}) # RUF025
|
||||
|
||||
|
||||
def func():
|
||||
{n: True for n in [1, 2, 2]} # RUF025
|
||||
|
||||
|
||||
def func():
|
||||
{n: b"hello" for n in (1, 2, 2)} # RUF025
|
||||
|
||||
|
||||
def func():
|
||||
{n: ... for n in [1, 2, 3]} # RUF025
|
||||
|
||||
|
||||
def func():
|
||||
{n: False for n in {1: "a", 2: "b"}} # RUF025
|
||||
|
||||
|
||||
def func():
|
||||
{(a, b): 1 for (a, b) in [(1, 2), (3, 4)]} # RUF025
|
||||
|
||||
|
||||
def func():
|
||||
def f():
|
||||
return 1
|
||||
|
||||
a = f()
|
||||
{n: a for n in [1, 2, 3]} # RUF025
|
||||
|
||||
|
||||
def func():
|
||||
values = ["a", "b", "c"]
|
||||
[{n: values for n in [1, 2, 3]}] # RUF025
|
||||
|
||||
|
||||
# Non-violation cases: RUF025
|
||||
|
||||
|
||||
def func():
|
||||
{n: 1 for n in [1, 2, 3, 4, 5] if n < 3} # OK
|
||||
|
||||
|
||||
def func():
|
||||
{n: 1 for c in [1, 2, 3, 4, 5] for n in [1, 2, 3] if c < 3} # OK
|
||||
|
||||
|
||||
def func():
|
||||
def f():
|
||||
pass
|
||||
|
||||
{n: f() for n in [1, 2, 3]} # OK
|
||||
|
||||
|
||||
def func():
|
||||
{n: n for n in [1, 2, 3, 4, 5]} # OK
|
||||
|
||||
|
||||
def func():
|
||||
def f():
|
||||
return {n: 1 for c in [1, 2, 3, 4, 5] for n in [1, 2, 3]} # OK
|
||||
|
||||
f()
|
||||
|
||||
|
||||
def func():
|
||||
{(a, b): a + b for (a, b) in [(1, 2), (3, 4)]} # OK
|
||||
120
crates/ruff_linter/resources/test/fixtures/ruff/RUF026.py
vendored
Normal file
120
crates/ruff_linter/resources/test/fixtures/ruff/RUF026.py
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
from collections import defaultdict
|
||||
|
||||
# Violation cases: RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=None) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=int) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=float) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=dict) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=list) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=tuple) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
def foo():
|
||||
pass
|
||||
|
||||
defaultdict(default_factory=foo) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=lambda: 1) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
from collections import deque
|
||||
|
||||
defaultdict(default_factory=deque) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
class MyCallable:
|
||||
def __call__(self):
|
||||
pass
|
||||
|
||||
defaultdict(default_factory=MyCallable()) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=tuple, member=1) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(member=1, default_factory=tuple) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(member=1, default_factory=tuple,) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(
|
||||
member=1,
|
||||
default_factory=tuple,
|
||||
) # RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(
|
||||
default_factory=tuple,
|
||||
member=1,
|
||||
) # RUF026
|
||||
|
||||
|
||||
# Non-violation cases: RUF026
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=1) # OK
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory="wdefwef") # OK
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=[1, 2, 3]) # OK
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict() # OK
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(int) # OK
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(list) # OK
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(dict) # OK
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(dict, defaultdict=list) # OK
|
||||
|
||||
|
||||
def func():
|
||||
def constant_factory(value):
|
||||
return lambda: value
|
||||
|
||||
defaultdict(constant_factory("<missing>"))
|
||||
@@ -125,6 +125,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::SliceCopy) {
|
||||
refurb::rules::slice_copy(checker, subscript);
|
||||
}
|
||||
if checker.enabled(Rule::PotentialIndexError) {
|
||||
pylint::rules::potential_index_error(checker, value, slice);
|
||||
}
|
||||
|
||||
pandas_vet::rules::subscript(checker, value, expr);
|
||||
}
|
||||
@@ -974,9 +977,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::SslInsecureVersion) {
|
||||
flake8_bandit::rules::ssl_insecure_version(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::MutableFromkeysValue) {
|
||||
ruff::rules::mutable_fromkeys_value(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnsortedDunderAll) {
|
||||
ruff::rules::sort_dunder_all_extend_call(checker, call);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::DefaultFactoryKwarg) {
|
||||
ruff::rules::default_factory_kwarg(checker, call);
|
||||
}
|
||||
}
|
||||
Expr::Dict(dict) => {
|
||||
if checker.any_enabled(&[
|
||||
@@ -1416,11 +1426,17 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
|
||||
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::UnnecessaryComprehension) {
|
||||
flake8_comprehensions::rules::unnecessary_dict_comprehension(
|
||||
checker, expr, key, value, generators,
|
||||
);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::UnnecessaryDictComprehensionForIterable) {
|
||||
ruff::rules::unnecessary_dict_comprehension_for_iterable(checker, dict_comp);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::FunctionUsesLoopVariable) {
|
||||
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr));
|
||||
}
|
||||
|
||||
@@ -507,6 +507,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::NoSlotsInNamedtupleSubclass) {
|
||||
flake8_slots::rules::no_slots_in_namedtuple_subclass(checker, stmt, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::NonSlotAssignment) {
|
||||
pylint::rules::non_slot_assignment(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::SingleStringSlots) {
|
||||
pylint::rules::single_string_slots(checker, class_def);
|
||||
}
|
||||
@@ -1069,13 +1072,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
ruff::rules::sort_dunder_all_aug_assign(checker, aug_assign);
|
||||
}
|
||||
}
|
||||
Stmt::If(
|
||||
if_ @ ast::StmtIf {
|
||||
test,
|
||||
elif_else_clauses,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
Stmt::If(if_ @ ast::StmtIf { test, .. }) => {
|
||||
if checker.enabled(Rule::TooManyNestedBlocks) {
|
||||
pylint::rules::too_many_nested_blocks(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::EmptyTypeCheckingBlock) {
|
||||
if typing::is_type_checking_block(if_, &checker.semantic) {
|
||||
flake8_type_checking::rules::empty_type_checking_block(checker, if_);
|
||||
@@ -1092,7 +1092,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::IfWithSameArms) {
|
||||
flake8_simplify::rules::if_with_same_arms(checker, checker.locator, if_);
|
||||
flake8_simplify::rules::if_with_same_arms(checker, if_);
|
||||
}
|
||||
if checker.enabled(Rule::NeedlessBool) {
|
||||
flake8_simplify::rules::needless_bool(checker, if_);
|
||||
@@ -1117,9 +1117,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
pyupgrade::rules::outdated_version_block(checker, if_);
|
||||
}
|
||||
if checker.enabled(Rule::CollapsibleElseIf) {
|
||||
if let Some(diagnostic) = pylint::rules::collapsible_else_if(elif_else_clauses) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
pylint::rules::collapsible_else_if(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::CheckAndRemoveFromSet) {
|
||||
refurb::rules::check_and_remove_from_set(checker, if_);
|
||||
@@ -1210,6 +1208,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => {
|
||||
if checker.enabled(Rule::TooManyNestedBlocks) {
|
||||
pylint::rules::too_many_nested_blocks(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::AssertRaisesException) {
|
||||
flake8_bugbear::rules::assert_raises_exception(checker, items);
|
||||
}
|
||||
@@ -1237,6 +1238,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
Stmt::While(while_stmt @ ast::StmtWhile { body, orelse, .. }) => {
|
||||
if checker.enabled(Rule::TooManyNestedBlocks) {
|
||||
pylint::rules::too_many_nested_blocks(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::FunctionUsesLoopVariable) {
|
||||
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Stmt(stmt));
|
||||
}
|
||||
@@ -1260,6 +1264,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
range: _,
|
||||
},
|
||||
) => {
|
||||
if checker.enabled(Rule::TooManyNestedBlocks) {
|
||||
pylint::rules::too_many_nested_blocks(checker, stmt);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::EnumerateForLoop,
|
||||
Rule::IncorrectDictIterator,
|
||||
@@ -1324,6 +1331,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
finalbody,
|
||||
..
|
||||
}) => {
|
||||
if checker.enabled(Rule::TooManyNestedBlocks) {
|
||||
pylint::rules::too_many_nested_blocks(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::JumpStatementInFinally) {
|
||||
flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody);
|
||||
}
|
||||
@@ -1458,6 +1468,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.settings.rules.enabled(Rule::UnsortedDunderAll) {
|
||||
ruff::rules::sort_dunder_all_assign(checker, assign);
|
||||
}
|
||||
if checker.enabled(Rule::UnsortedDunderSlots) {
|
||||
ruff::rules::sort_dunder_slots_assign(checker, assign);
|
||||
}
|
||||
if checker.source_type.is_stub() {
|
||||
if checker.any_enabled(&[
|
||||
Rule::UnprefixedTypeParam,
|
||||
@@ -1531,6 +1544,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.settings.rules.enabled(Rule::UnsortedDunderAll) {
|
||||
ruff::rules::sort_dunder_all_ann_assign(checker, assign_stmt);
|
||||
}
|
||||
if checker.enabled(Rule::UnsortedDunderSlots) {
|
||||
ruff::rules::sort_dunder_slots_ann_assign(checker, assign_stmt);
|
||||
}
|
||||
if checker.source_type.is_stub() {
|
||||
if let Some(value) = value {
|
||||
if checker.enabled(Rule::AssignmentDefaultInStub) {
|
||||
|
||||
@@ -721,6 +721,21 @@ where
|
||||
AnnotationContext::RuntimeEvaluated => {
|
||||
self.visit_runtime_evaluated_annotation(annotation);
|
||||
}
|
||||
AnnotationContext::TypingOnly
|
||||
if flake8_type_checking::helpers::is_dataclass_meta_annotation(
|
||||
annotation,
|
||||
self.semantic(),
|
||||
) =>
|
||||
{
|
||||
if let Expr::Subscript(subscript) = &**annotation {
|
||||
// Ex) `InitVar[str]`
|
||||
self.visit_runtime_required_annotation(&subscript.value);
|
||||
self.visit_annotation(&subscript.slice);
|
||||
} else {
|
||||
// Ex) `InitVar`
|
||||
self.visit_runtime_required_annotation(annotation);
|
||||
}
|
||||
}
|
||||
AnnotationContext::TypingOnly => self.visit_annotation(annotation),
|
||||
}
|
||||
|
||||
|
||||
@@ -224,11 +224,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "E0116") => (RuleGroup::Stable, rules::pylint::rules::ContinueInFinally),
|
||||
(Pylint, "E0117") => (RuleGroup::Stable, rules::pylint::rules::NonlocalWithoutBinding),
|
||||
(Pylint, "E0118") => (RuleGroup::Stable, rules::pylint::rules::LoadBeforeGlobalDeclaration),
|
||||
(Pylint, "E0237") => (RuleGroup::Stable, rules::pylint::rules::NonSlotAssignment),
|
||||
(Pylint, "E0241") => (RuleGroup::Stable, rules::pylint::rules::DuplicateBases),
|
||||
(Pylint, "E0302") => (RuleGroup::Stable, rules::pylint::rules::UnexpectedSpecialMethodSignature),
|
||||
(Pylint, "E0307") => (RuleGroup::Stable, rules::pylint::rules::InvalidStrReturnType),
|
||||
(Pylint, "E0604") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllObject),
|
||||
(Pylint, "E0605") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllFormat),
|
||||
(Pylint, "E0643") => (RuleGroup::Preview, rules::pylint::rules::PotentialIndexError),
|
||||
(Pylint, "E0704") => (RuleGroup::Preview, rules::pylint::rules::MisplacedBareRaise),
|
||||
(Pylint, "E1132") => (RuleGroup::Preview, rules::pylint::rules::RepeatedKeywordArgument),
|
||||
(Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync),
|
||||
@@ -259,6 +261,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
|
||||
(Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositional),
|
||||
(Pylint, "R1701") => (RuleGroup::Stable, rules::pylint::rules::RepeatedIsinstanceCalls),
|
||||
(Pylint, "R1702") => (RuleGroup::Preview, rules::pylint::rules::TooManyNestedBlocks),
|
||||
(Pylint, "R1704") => (RuleGroup::Preview, rules::pylint::rules::RedefinedArgumentFromLocal),
|
||||
(Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn),
|
||||
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
|
||||
@@ -925,6 +928,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion),
|
||||
(Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators),
|
||||
(Ruff, "022") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderAll),
|
||||
(Ruff, "023") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderSlots),
|
||||
(Ruff, "024") => (RuleGroup::Preview, rules::ruff::rules::MutableFromkeysValue),
|
||||
(Ruff, "025") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryDictComprehensionForIterable),
|
||||
(Ruff, "026") => (RuleGroup::Preview, rules::ruff::rules::DefaultFactoryKwarg),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
|
||||
|
||||
|
||||
@@ -795,6 +795,23 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undefined_name() -> Result<(), NotebookError> {
|
||||
let actual = notebook_path("undefined_name.ipynb");
|
||||
let expected = notebook_path("undefined_name.ipynb");
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = assert_notebook_path(
|
||||
&actual,
|
||||
expected,
|
||||
&settings::LinterSettings::for_rule(Rule::UndefinedName),
|
||||
)?;
|
||||
assert_messages!(messages, actual, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_consistency() -> Result<()> {
|
||||
let actual_path = notebook_path("before_fix.ipynb");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_source_file::Locator;
|
||||
@@ -30,14 +30,16 @@ use super::super::detection::comment_contains_code;
|
||||
#[violation]
|
||||
pub struct CommentedOutCode;
|
||||
|
||||
impl AlwaysFixableViolation for CommentedOutCode {
|
||||
impl Violation for CommentedOutCode {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Found commented-out code")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Remove commented-out code".to_string()
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!("Remove commented-out code"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +67,6 @@ pub(crate) fn commented_out_code(
|
||||
// Verify that the comment is on its own line, and that it contains code.
|
||||
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
|
||||
let mut diagnostic = Diagnostic::new(CommentedOutCode, *range);
|
||||
|
||||
diagnostic.set_fix(Fix::display_only_edit(Edit::range_deletion(
|
||||
locator.full_lines_range(*range),
|
||||
)));
|
||||
|
||||
@@ -9,4 +9,3 @@ EXE001_1.py:1:1: EXE001 Shebang is present but file is not executable
|
||||
3 | if __name__ == '__main__':
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -7,5 +7,3 @@ EXE002_1.py:1:1: EXE002 The file is executable but no shebang is present
|
||||
| EXE002
|
||||
2 | print('I should be executable.')
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::iter;
|
||||
|
||||
use itertools::Either::{Left, Right};
|
||||
|
||||
use ruff_python_semantic::{analyze, SemanticModel};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use ruff_python_ast::{self as ast, Arguments, BoolOp, Expr, ExprContext, Identifier};
|
||||
@@ -36,6 +37,14 @@ use crate::checkers::ast::Checker;
|
||||
/// print("Greetings!")
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is unsafe, as in some cases, it will be unable to determine
|
||||
/// whether the argument to an existing `.startswith` or `.endswith` call is a
|
||||
/// tuple. For example, given `msg.startswith(x) or msg.startswith(y)`, if `x`
|
||||
/// or `y` is a tuple, and the semantic model is unable to detect it as such,
|
||||
/// the rule will suggest `msg.startswith((x, y))`, which will error at
|
||||
/// runtime.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `str.startswith`](https://docs.python.org/3/library/stdtypes.html#str.startswith)
|
||||
/// - [Python documentation: `str.endswith`](https://docs.python.org/3/library/stdtypes.html#str.endswith)
|
||||
@@ -84,10 +93,14 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !(args.len() == 1 && keywords.is_empty()) {
|
||||
if !keywords.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let [arg] = args.as_slice() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
@@ -99,6 +112,13 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
|
||||
continue;
|
||||
};
|
||||
|
||||
// If the argument is bound to a tuple, skip it, since we don't want to suggest
|
||||
// `startswith((x, y))` where `x` or `y` are tuples. (Tuple literals are okay, since we
|
||||
// inline them below.)
|
||||
if is_bound_to_tuple(arg, checker.semantic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
duplicates
|
||||
.entry((attr.as_str(), arg_name.as_str()))
|
||||
.or_insert_with(Vec::new)
|
||||
@@ -149,7 +169,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
|
||||
Right(iter::once(*value))
|
||||
}
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.cloned()
|
||||
.collect(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
@@ -202,3 +222,18 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the expression definitively resolves to a tuple (e.g., `x` in `x = (1, 2)`).
|
||||
fn is_bound_to_tuple(arg: &Expr, semantic: &SemanticModel) -> bool {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = arg else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(binding_id) = semantic.lookup_symbol(id.as_str()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let binding = semantic.binding(binding_id);
|
||||
|
||||
analyze::typing::is_tuple(binding, semantic)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ PIE810.py:10:1: PIE810 [*] Call `startswith` once with a `tuple`
|
||||
10 | obj.endswith(foo) or obj.startswith(foo) or obj.startswith("foo")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE810
|
||||
11 |
|
||||
12 | # ok
|
||||
12 | def func():
|
||||
|
|
||||
= help: Merge into a single `startswith` call
|
||||
|
||||
@@ -100,7 +100,47 @@ PIE810.py:10:1: PIE810 [*] Call `startswith` once with a `tuple`
|
||||
10 |-obj.endswith(foo) or obj.startswith(foo) or obj.startswith("foo")
|
||||
10 |+obj.endswith(foo) or obj.startswith((foo, "foo"))
|
||||
11 11 |
|
||||
12 12 | # ok
|
||||
13 13 | obj.startswith(("foo", "bar"))
|
||||
12 12 | def func():
|
||||
13 13 | msg = "hello world"
|
||||
|
||||
PIE810.py:19:8: PIE810 [*] Call `startswith` once with a `tuple`
|
||||
|
|
||||
17 | z = "w"
|
||||
18 |
|
||||
19 | if msg.startswith(x) or msg.startswith(y) or msg.startswith(z): # Error
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE810
|
||||
20 | print("yes")
|
||||
|
|
||||
= help: Merge into a single `startswith` call
|
||||
|
||||
ℹ Unsafe fix
|
||||
16 16 | y = ("h", "e", "l", "l", "o")
|
||||
17 17 | z = "w"
|
||||
18 18 |
|
||||
19 |- if msg.startswith(x) or msg.startswith(y) or msg.startswith(z): # Error
|
||||
19 |+ if msg.startswith((x, z)) or msg.startswith(y): # Error
|
||||
20 20 | print("yes")
|
||||
21 21 |
|
||||
22 22 | def func():
|
||||
|
||||
PIE810.py:25:8: PIE810 [*] Call `startswith` once with a `tuple`
|
||||
|
|
||||
23 | msg = "hello world"
|
||||
24 |
|
||||
25 | if msg.startswith(("h", "e", "l", "l", "o")) or msg.startswith("h") or msg.startswith("w"): # Error
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE810
|
||||
26 | print("yes")
|
||||
|
|
||||
= help: Merge into a single `startswith` call
|
||||
|
||||
ℹ Unsafe fix
|
||||
22 22 | def func():
|
||||
23 23 | msg = "hello world"
|
||||
24 24 |
|
||||
25 |- if msg.startswith(("h", "e", "l", "l", "o")) or msg.startswith("h") or msg.startswith("w"): # Error
|
||||
25 |+ if msg.startswith(("h", "e", "l", "l", "o", "h", "w")): # Error
|
||||
26 26 | print("yes")
|
||||
27 27 |
|
||||
28 28 | # ok
|
||||
|
||||
|
||||
|
||||
@@ -226,6 +226,10 @@ impl Violation for PytestParametrizeValuesWrongType {
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as tests that rely on mutable global
|
||||
/// state may be affected by removing duplicate test cases.
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
|
||||
#[violation]
|
||||
|
||||
@@ -11,10 +11,11 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
#[test_case(Rule::UnnecessaryReturnNone, Path::new("RET501.py"))]
|
||||
#[test_case(Rule::ImplicitReturnValue, Path::new("RET502.py"))]
|
||||
@@ -33,4 +34,25 @@ mod tests {
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::SuperfluousElseReturn, Path::new("RET505.py"))]
|
||||
#[test_case(Rule::SuperfluousElseRaise, Path::new("RET506.py"))]
|
||||
#[test_case(Rule::SuperfluousElseContinue, Path::new("RET507.py"))]
|
||||
#[test_case(Rule::SuperfluousElseBreak, Path::new("RET508.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_return").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use std::ops::Add;
|
||||
|
||||
use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Violation};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, FixAvailability, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
@@ -11,13 +12,17 @@ use ruff_python_ast::helpers::{is_const_false, is_const_true};
|
||||
use ruff_python_ast::stmt_if::elif_else_range;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::whitespace::indentation;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_trivia::is_python_whitespace;
|
||||
use ruff_python_trivia::{is_python_whitespace, SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::flake8_return::helpers::end_of_last_statement;
|
||||
use crate::rules::pyupgrade::fixes::adjust_indentation;
|
||||
|
||||
use super::super::branch::Branch;
|
||||
use super::super::helpers::result_exists;
|
||||
@@ -210,11 +215,17 @@ pub struct SuperfluousElseReturn {
|
||||
}
|
||||
|
||||
impl Violation for SuperfluousElseReturn {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let SuperfluousElseReturn { branch } = self;
|
||||
format!("Unnecessary `{branch}` after `return` statement")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let SuperfluousElseReturn { branch } = self;
|
||||
Some(format!("Remove unnecessary `{branch}`"))
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
@@ -248,11 +259,17 @@ pub struct SuperfluousElseRaise {
|
||||
}
|
||||
|
||||
impl Violation for SuperfluousElseRaise {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let SuperfluousElseRaise { branch } = self;
|
||||
format!("Unnecessary `{branch}` after `raise` statement")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let SuperfluousElseRaise { branch } = self;
|
||||
Some(format!("Remove unnecessary `{branch}`"))
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
@@ -288,11 +305,17 @@ pub struct SuperfluousElseContinue {
|
||||
}
|
||||
|
||||
impl Violation for SuperfluousElseContinue {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let SuperfluousElseContinue { branch } = self;
|
||||
format!("Unnecessary `{branch}` after `continue` statement")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let SuperfluousElseContinue { branch } = self;
|
||||
Some(format!("Remove unnecessary `{branch}`"))
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
@@ -328,11 +351,17 @@ pub struct SuperfluousElseBreak {
|
||||
}
|
||||
|
||||
impl Violation for SuperfluousElseBreak {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let SuperfluousElseBreak { branch } = self;
|
||||
format!("Unnecessary `{branch}` after `break` statement")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let SuperfluousElseBreak { branch } = self;
|
||||
Some(format!("Remove unnecessary `{branch}`"))
|
||||
}
|
||||
}
|
||||
|
||||
/// RET501
|
||||
@@ -368,9 +397,11 @@ fn implicit_return_value(checker: &mut Checker, stack: &Stack) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the `func` is a known function that never returns.
|
||||
/// Return `true` if the `func` appears to be non-returning.
|
||||
fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
semantic.resolve_call_path(func).is_some_and(|call_path| {
|
||||
// First, look for known functions that never return from the standard library and popular
|
||||
// libraries.
|
||||
if semantic.resolve_call_path(func).is_some_and(|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["" | "builtins" | "sys" | "_thread" | "pytest", "exit"]
|
||||
@@ -379,7 +410,32 @@ fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
| ["_winapi", "ExitProcess"]
|
||||
| ["pytest", "fail" | "skip" | "xfail"]
|
||||
) || semantic.match_typing_call_path(&call_path, "assert_never")
|
||||
})
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Second, look for `NoReturn` annotations on the return type.
|
||||
let Some(func_binding) = semantic.lookup_attribute(func) else {
|
||||
return false;
|
||||
};
|
||||
let Some(node_id) = semantic.binding(func_binding).source else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Stmt::FunctionDef(ast::StmtFunctionDef { returns, .. }) = semantic.statement(node_id)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(returns) = returns.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(call_path) = semantic.resolve_call_path(returns) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
semantic.match_typing_call_path(&call_path, "NoReturn")
|
||||
}
|
||||
|
||||
/// RET503
|
||||
@@ -575,42 +631,82 @@ fn superfluous_else_node(
|
||||
};
|
||||
for child in if_elif_body {
|
||||
if child.is_return_stmt() {
|
||||
let diagnostic = Diagnostic::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
SuperfluousElseReturn { branch },
|
||||
elif_else_range(elif_else, checker.locator().contents())
|
||||
.unwrap_or_else(|| elif_else.range()),
|
||||
);
|
||||
if checker.enabled(diagnostic.kind.rule()) {
|
||||
if checker.settings.preview.is_enabled() {
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_else(
|
||||
elif_else,
|
||||
checker.locator(),
|
||||
checker.indexer(),
|
||||
checker.stylist(),
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
return true;
|
||||
} else if child.is_break_stmt() {
|
||||
let diagnostic = Diagnostic::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
SuperfluousElseBreak { branch },
|
||||
elif_else_range(elif_else, checker.locator().contents())
|
||||
.unwrap_or_else(|| elif_else.range()),
|
||||
);
|
||||
if checker.enabled(diagnostic.kind.rule()) {
|
||||
if checker.settings.preview.is_enabled() {
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_else(
|
||||
elif_else,
|
||||
checker.locator(),
|
||||
checker.indexer(),
|
||||
checker.stylist(),
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
return true;
|
||||
} else if child.is_raise_stmt() {
|
||||
let diagnostic = Diagnostic::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
SuperfluousElseRaise { branch },
|
||||
elif_else_range(elif_else, checker.locator().contents())
|
||||
.unwrap_or_else(|| elif_else.range()),
|
||||
);
|
||||
if checker.enabled(diagnostic.kind.rule()) {
|
||||
if checker.settings.preview.is_enabled() {
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_else(
|
||||
elif_else,
|
||||
checker.locator(),
|
||||
checker.indexer(),
|
||||
checker.stylist(),
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
return true;
|
||||
} else if child.is_continue_stmt() {
|
||||
let diagnostic = Diagnostic::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
SuperfluousElseContinue { branch },
|
||||
elif_else_range(elif_else, checker.locator().contents())
|
||||
.unwrap_or_else(|| elif_else.range()),
|
||||
);
|
||||
if checker.enabled(diagnostic.kind.rule()) {
|
||||
if checker.settings.preview.is_enabled() {
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_else(
|
||||
elif_else,
|
||||
checker.locator(),
|
||||
checker.indexer(),
|
||||
checker.stylist(),
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
return true;
|
||||
@@ -641,7 +737,7 @@ pub(crate) fn function(checker: &mut Checker, body: &[Stmt], returns: Option<&Ex
|
||||
|
||||
// Traverse the function body, to collect the stack.
|
||||
let stack = {
|
||||
let mut visitor = ReturnVisitor::default();
|
||||
let mut visitor = ReturnVisitor::new(checker.semantic());
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
@@ -688,3 +784,83 @@ pub(crate) fn function(checker: &mut Checker, body: &[Stmt], returns: Option<&Ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a [`Fix`] to remove an `else` or `elif` clause.
|
||||
fn remove_else(
|
||||
elif_else: &ElifElseClause,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Fix> {
|
||||
if elif_else.test.is_some() {
|
||||
// Ex) `elif` -> `if`
|
||||
Ok(Fix::safe_edit(Edit::deletion(
|
||||
elif_else.start(),
|
||||
elif_else.start() + TextSize::from(2),
|
||||
)))
|
||||
} else {
|
||||
// the start of the line where the `else`` is
|
||||
let else_line_start = locator.line_start(elif_else.start());
|
||||
|
||||
// making a tokenizer to find the Colon for the `else`, not always on the same line!
|
||||
let mut else_line_tokenizer =
|
||||
SimpleTokenizer::starts_at(elif_else.start(), locator.contents());
|
||||
|
||||
// find the Colon for the `else`
|
||||
let Some(else_colon) =
|
||||
else_line_tokenizer.find(|token| token.kind == SimpleTokenKind::Colon)
|
||||
else {
|
||||
return Err(anyhow::anyhow!("Cannot find `:` in `else` statement"));
|
||||
};
|
||||
|
||||
// get the indentation of the `else`, since that is the indent level we want to end with
|
||||
let Some(desired_indentation) = indentation(locator, elif_else) else {
|
||||
return Err(anyhow::anyhow!("Compound statement cannot be inlined"));
|
||||
};
|
||||
|
||||
// If the statement is on the same line as the `else`, just remove the `else: `.
|
||||
// Ex) `else: return True` -> `return True`
|
||||
let Some(first) = elif_else.body.first() else {
|
||||
return Err(anyhow::anyhow!("`else` statement has no body"));
|
||||
};
|
||||
if indexer.in_multi_statement_line(first, locator) {
|
||||
return Ok(Fix::safe_edit(Edit::deletion(
|
||||
elif_else.start(),
|
||||
first.start(),
|
||||
)));
|
||||
}
|
||||
|
||||
// we're deleting the `else`, and it's Colon, and the rest of the line(s) they're on,
|
||||
// so here we get the last position of the line the Colon is on
|
||||
let else_colon_end = locator.full_line_end(else_colon.end());
|
||||
|
||||
// if there is a comment on the same line as the Colon, let's keep it
|
||||
// and give it the proper indentation once we unindent it
|
||||
let else_comment_after_colon = else_line_tokenizer
|
||||
.find(|token| token.kind.is_comment())
|
||||
.and_then(|token| {
|
||||
if token.kind == SimpleTokenKind::Comment && token.start() < else_colon_end {
|
||||
return Some(format!(
|
||||
"{desired_indentation}{}{}",
|
||||
locator.slice(token),
|
||||
stylist.line_ending().as_str(),
|
||||
));
|
||||
}
|
||||
None
|
||||
})
|
||||
.unwrap_or(String::new());
|
||||
|
||||
let indented = adjust_indentation(
|
||||
TextRange::new(else_colon_end, elif_else.end()),
|
||||
desired_indentation,
|
||||
locator,
|
||||
stylist,
|
||||
)?;
|
||||
|
||||
Ok(Fix::safe_edit(Edit::replacement(
|
||||
format!("{else_comment_after_colon}{indented}"),
|
||||
else_line_start,
|
||||
elif_else.end(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,403 +1,456 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_return/mod.rs
|
||||
---
|
||||
RET503.py:20:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:21:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
18 | # if/elif/else
|
||||
19 | def x(y):
|
||||
20 | if not y:
|
||||
19 | # if/elif/else
|
||||
20 | def x(y):
|
||||
21 | if not y:
|
||||
| _____^
|
||||
21 | | return 1
|
||||
22 | | return 1
|
||||
| |________________^ RET503
|
||||
22 | # error
|
||||
23 | # error
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
19 19 | def x(y):
|
||||
20 20 | if not y:
|
||||
21 21 | return 1
|
||||
22 |+ return None
|
||||
22 23 | # error
|
||||
23 24 |
|
||||
20 20 | def x(y):
|
||||
21 21 | if not y:
|
||||
22 22 | return 1
|
||||
23 |+ return None
|
||||
23 24 | # error
|
||||
24 25 |
|
||||
25 26 |
|
||||
|
||||
RET503.py:27:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:28:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
25 | def x(y):
|
||||
26 | if not y:
|
||||
27 | print() # error
|
||||
26 | def x(y):
|
||||
27 | if not y:
|
||||
28 | print() # error
|
||||
| ^^^^^^^ RET503
|
||||
28 | else:
|
||||
29 | return 2
|
||||
29 | else:
|
||||
30 | return 2
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
25 25 | def x(y):
|
||||
26 26 | if not y:
|
||||
27 27 | print() # error
|
||||
28 |+ return None
|
||||
28 29 | else:
|
||||
29 30 | return 2
|
||||
30 31 |
|
||||
26 26 | def x(y):
|
||||
27 27 | if not y:
|
||||
28 28 | print() # error
|
||||
29 |+ return None
|
||||
29 30 | else:
|
||||
30 31 | return 2
|
||||
31 32 |
|
||||
|
||||
RET503.py:36:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:37:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
34 | return 1
|
||||
35 |
|
||||
36 | print() # error
|
||||
35 | return 1
|
||||
36 |
|
||||
37 | print() # error
|
||||
| ^^^^^^^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
34 34 | return 1
|
||||
35 35 |
|
||||
36 36 | print() # error
|
||||
37 |+ return None
|
||||
37 38 |
|
||||
35 35 | return 1
|
||||
36 36 |
|
||||
37 37 | print() # error
|
||||
38 |+ return None
|
||||
38 39 |
|
||||
39 40 | # for
|
||||
39 40 |
|
||||
40 41 | # for
|
||||
|
||||
RET503.py:41:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:42:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
39 | # for
|
||||
40 | def x(y):
|
||||
41 | for i in range(10):
|
||||
40 | # for
|
||||
41 | def x(y):
|
||||
42 | for i in range(10):
|
||||
| _____^
|
||||
42 | | if i > 10:
|
||||
43 | | return i
|
||||
43 | | if i > 10:
|
||||
44 | | return i
|
||||
| |____________________^ RET503
|
||||
44 | # error
|
||||
45 | # error
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
41 41 | for i in range(10):
|
||||
42 42 | if i > 10:
|
||||
43 43 | return i
|
||||
44 |+ return None
|
||||
44 45 | # error
|
||||
45 46 |
|
||||
42 42 | for i in range(10):
|
||||
43 43 | if i > 10:
|
||||
44 44 | return i
|
||||
45 |+ return None
|
||||
45 46 | # error
|
||||
46 47 |
|
||||
47 48 |
|
||||
|
||||
RET503.py:52:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:53:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
50 | return i
|
||||
51 | else:
|
||||
52 | print() # error
|
||||
51 | return i
|
||||
52 | else:
|
||||
53 | print() # error
|
||||
| ^^^^^^^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
50 50 | return i
|
||||
51 51 | else:
|
||||
52 52 | print() # error
|
||||
53 |+ return None
|
||||
53 54 |
|
||||
51 51 | return i
|
||||
52 52 | else:
|
||||
53 53 | print() # error
|
||||
54 |+ return None
|
||||
54 55 |
|
||||
55 56 | # A nonexistent function
|
||||
55 56 |
|
||||
56 57 | # A nonexistent function
|
||||
|
||||
RET503.py:59:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:60:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
57 | if x > 0:
|
||||
58 | return False
|
||||
59 | no_such_function() # error
|
||||
58 | if x > 0:
|
||||
59 | return False
|
||||
60 | no_such_function() # error
|
||||
| ^^^^^^^^^^^^^^^^^^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
57 57 | if x > 0:
|
||||
58 58 | return False
|
||||
59 59 | no_such_function() # error
|
||||
60 |+ return None
|
||||
60 61 |
|
||||
58 58 | if x > 0:
|
||||
59 59 | return False
|
||||
60 60 | no_such_function() # error
|
||||
61 |+ return None
|
||||
61 62 |
|
||||
62 63 | # A function that does return the control
|
||||
62 63 |
|
||||
63 64 | # A function that does return the control
|
||||
|
||||
RET503.py:66:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:67:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
64 | if x > 0:
|
||||
65 | return False
|
||||
66 | print("", end="") # error
|
||||
65 | if x > 0:
|
||||
66 | return False
|
||||
67 | print("", end="") # error
|
||||
| ^^^^^^^^^^^^^^^^^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
64 64 | if x > 0:
|
||||
65 65 | return False
|
||||
66 66 | print("", end="") # error
|
||||
67 |+ return None
|
||||
67 68 |
|
||||
65 65 | if x > 0:
|
||||
66 66 | return False
|
||||
67 67 | print("", end="") # error
|
||||
68 |+ return None
|
||||
68 69 |
|
||||
69 70 | ###
|
||||
69 70 |
|
||||
70 71 | ###
|
||||
|
||||
RET503.py:82:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:83:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
80 | # last line in while loop
|
||||
81 | def x(y):
|
||||
82 | while i > 0:
|
||||
81 | # last line in while loop
|
||||
82 | def x(y):
|
||||
83 | while i > 0:
|
||||
| _____^
|
||||
83 | | if y > 0:
|
||||
84 | | return 1
|
||||
85 | | y += 1
|
||||
84 | | if y > 0:
|
||||
85 | | return 1
|
||||
86 | | y += 1
|
||||
| |______________^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
83 83 | if y > 0:
|
||||
84 84 | return 1
|
||||
85 85 | y += 1
|
||||
86 |+ return None
|
||||
86 87 |
|
||||
84 84 | if y > 0:
|
||||
85 85 | return 1
|
||||
86 86 | y += 1
|
||||
87 |+ return None
|
||||
87 88 |
|
||||
88 89 | # exclude empty functions
|
||||
88 89 |
|
||||
89 90 | # exclude empty functions
|
||||
|
||||
RET503.py:113:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:114:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
111 | # return value within loop
|
||||
112 | def bar1(x, y, z):
|
||||
113 | for i in x:
|
||||
112 | # return value within loop
|
||||
113 | def bar1(x, y, z):
|
||||
114 | for i in x:
|
||||
| _____^
|
||||
114 | | if i > y:
|
||||
115 | | break
|
||||
116 | | return z
|
||||
115 | | if i > y:
|
||||
116 | | break
|
||||
117 | | return z
|
||||
| |________________^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
114 114 | if i > y:
|
||||
115 115 | break
|
||||
116 116 | return z
|
||||
117 |+ return None
|
||||
117 118 |
|
||||
115 115 | if i > y:
|
||||
116 116 | break
|
||||
117 117 | return z
|
||||
118 |+ return None
|
||||
118 119 |
|
||||
119 120 | def bar3(x, y, z):
|
||||
119 120 |
|
||||
120 121 | def bar3(x, y, z):
|
||||
|
||||
RET503.py:120:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:121:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
119 | def bar3(x, y, z):
|
||||
120 | for i in x:
|
||||
120 | def bar3(x, y, z):
|
||||
121 | for i in x:
|
||||
| _____^
|
||||
121 | | if i > y:
|
||||
122 | | if z:
|
||||
123 | | break
|
||||
124 | | else:
|
||||
125 | | return z
|
||||
126 | | return None
|
||||
122 | | if i > y:
|
||||
123 | | if z:
|
||||
124 | | break
|
||||
125 | | else:
|
||||
126 | | return z
|
||||
127 | | return None
|
||||
| |___________________^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
124 124 | else:
|
||||
125 125 | return z
|
||||
126 126 | return None
|
||||
127 |+ return None
|
||||
127 128 |
|
||||
125 125 | else:
|
||||
126 126 | return z
|
||||
127 127 | return None
|
||||
128 |+ return None
|
||||
128 129 |
|
||||
129 130 | def bar1(x, y, z):
|
||||
129 130 |
|
||||
130 131 | def bar1(x, y, z):
|
||||
|
||||
RET503.py:130:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:131:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
129 | def bar1(x, y, z):
|
||||
130 | for i in x:
|
||||
130 | def bar1(x, y, z):
|
||||
131 | for i in x:
|
||||
| _____^
|
||||
131 | | if i < y:
|
||||
132 | | continue
|
||||
133 | | return z
|
||||
132 | | if i < y:
|
||||
133 | | continue
|
||||
134 | | return z
|
||||
| |________________^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
131 131 | if i < y:
|
||||
132 132 | continue
|
||||
133 133 | return z
|
||||
134 |+ return None
|
||||
134 135 |
|
||||
132 132 | if i < y:
|
||||
133 133 | continue
|
||||
134 134 | return z
|
||||
135 |+ return None
|
||||
135 136 |
|
||||
136 137 | def bar3(x, y, z):
|
||||
136 137 |
|
||||
137 138 | def bar3(x, y, z):
|
||||
|
||||
RET503.py:137:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:138:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
136 | def bar3(x, y, z):
|
||||
137 | for i in x:
|
||||
137 | def bar3(x, y, z):
|
||||
138 | for i in x:
|
||||
| _____^
|
||||
138 | | if i < y:
|
||||
139 | | if z:
|
||||
140 | | continue
|
||||
141 | | else:
|
||||
142 | | return z
|
||||
143 | | return None
|
||||
139 | | if i < y:
|
||||
140 | | if z:
|
||||
141 | | continue
|
||||
142 | | else:
|
||||
143 | | return z
|
||||
144 | | return None
|
||||
| |___________________^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
141 141 | else:
|
||||
142 142 | return z
|
||||
143 143 | return None
|
||||
144 |+ return None
|
||||
144 145 |
|
||||
142 142 | else:
|
||||
143 143 | return z
|
||||
144 144 | return None
|
||||
145 |+ return None
|
||||
145 146 |
|
||||
146 147 | def prompts(self, foo):
|
||||
146 147 |
|
||||
147 148 | def prompts(self, foo):
|
||||
|
||||
RET503.py:274:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:275:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
272 | return False
|
||||
273 |
|
||||
274 | for value in values:
|
||||
273 | return False
|
||||
274 |
|
||||
275 | for value in values:
|
||||
| _____^
|
||||
275 | | print(value)
|
||||
276 | | print(value)
|
||||
| |____________________^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
273 273 |
|
||||
274 274 | for value in values:
|
||||
275 275 | print(value)
|
||||
276 |+ return None
|
||||
276 277 |
|
||||
274 274 |
|
||||
275 275 | for value in values:
|
||||
276 276 | print(value)
|
||||
277 |+ return None
|
||||
277 278 |
|
||||
278 279 | def while_true():
|
||||
278 279 |
|
||||
279 280 | def while_true():
|
||||
|
||||
RET503.py:291:13: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:292:13: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
289 | return 1
|
||||
290 | case 1:
|
||||
291 | print() # error
|
||||
290 | return 1
|
||||
291 | case 1:
|
||||
292 | print() # error
|
||||
| ^^^^^^^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
289 289 | return 1
|
||||
290 290 | case 1:
|
||||
291 291 | print() # error
|
||||
292 |+ return None
|
||||
292 293 |
|
||||
290 290 | return 1
|
||||
291 291 | case 1:
|
||||
292 292 | print() # error
|
||||
293 |+ return None
|
||||
293 294 |
|
||||
294 295 | def foo(baz: str) -> str:
|
||||
294 295 |
|
||||
295 296 | def foo(baz: str) -> str:
|
||||
|
||||
RET503.py:300:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:301:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
298 | def end_of_statement():
|
||||
299 | def example():
|
||||
300 | if True:
|
||||
299 | def end_of_statement():
|
||||
300 | def example():
|
||||
301 | if True:
|
||||
| _________^
|
||||
301 | | return ""
|
||||
302 | | return ""
|
||||
| |_____________________^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
299 299 | def example():
|
||||
300 300 | if True:
|
||||
301 301 | return ""
|
||||
302 |+ return None
|
||||
302 303 |
|
||||
300 300 | def example():
|
||||
301 301 | if True:
|
||||
302 302 | return ""
|
||||
303 |+ return None
|
||||
303 304 |
|
||||
304 305 | def example():
|
||||
304 305 |
|
||||
305 306 | def example():
|
||||
|
||||
RET503.py:305:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:306:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
304 | def example():
|
||||
305 | if True:
|
||||
305 | def example():
|
||||
306 | if True:
|
||||
| _________^
|
||||
306 | | return ""
|
||||
307 | | return ""
|
||||
| |_____________________^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
304 304 | def example():
|
||||
305 305 | if True:
|
||||
306 306 | return ""
|
||||
307 |+ return None
|
||||
307 308 |
|
||||
305 305 | def example():
|
||||
306 306 | if True:
|
||||
307 307 | return ""
|
||||
308 |+ return None
|
||||
308 309 |
|
||||
309 310 | def example():
|
||||
309 310 |
|
||||
310 311 | def example():
|
||||
|
||||
RET503.py:310:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:311:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
309 | def example():
|
||||
310 | if True:
|
||||
310 | def example():
|
||||
311 | if True:
|
||||
| _________^
|
||||
311 | | return "" # type: ignore
|
||||
312 | | return "" # type: ignore
|
||||
| |_____________________^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
309 309 | def example():
|
||||
310 310 | if True:
|
||||
311 311 | return "" # type: ignore
|
||||
312 |+ return None
|
||||
312 313 |
|
||||
310 310 | def example():
|
||||
311 311 | if True:
|
||||
312 312 | return "" # type: ignore
|
||||
313 |+ return None
|
||||
313 314 |
|
||||
314 315 | def example():
|
||||
314 315 |
|
||||
315 316 | def example():
|
||||
|
||||
RET503.py:315:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:316:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
314 | def example():
|
||||
315 | if True:
|
||||
315 | def example():
|
||||
316 | if True:
|
||||
| _________^
|
||||
316 | | return "" ;
|
||||
317 | | return "" ;
|
||||
| |_____________________^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
314 314 | def example():
|
||||
315 315 | if True:
|
||||
316 316 | return "" ;
|
||||
317 |+ return None
|
||||
317 318 |
|
||||
315 315 | def example():
|
||||
316 316 | if True:
|
||||
317 317 | return "" ;
|
||||
318 |+ return None
|
||||
318 319 |
|
||||
319 320 | def example():
|
||||
319 320 |
|
||||
320 321 | def example():
|
||||
|
||||
RET503.py:320:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:321:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
319 | def example():
|
||||
320 | if True:
|
||||
320 | def example():
|
||||
321 | if True:
|
||||
| _________^
|
||||
321 | | return "" \
|
||||
322 | | return "" \
|
||||
| |_____________________^ RET503
|
||||
322 | ; # type: ignore
|
||||
323 | ; # type: ignore
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
320 320 | if True:
|
||||
321 321 | return "" \
|
||||
322 322 | ; # type: ignore
|
||||
323 |+ return None
|
||||
323 324 |
|
||||
321 321 | if True:
|
||||
322 322 | return "" \
|
||||
323 323 | ; # type: ignore
|
||||
324 |+ return None
|
||||
324 325 |
|
||||
325 326 | def end_of_file():
|
||||
325 326 |
|
||||
326 327 | def end_of_file():
|
||||
|
||||
RET503.py:328:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
RET503.py:329:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
326 | if False:
|
||||
327 | return 1
|
||||
328 | x = 2 \
|
||||
327 | if False:
|
||||
328 | return 1
|
||||
329 | x = 2 \
|
||||
| ^^^^^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
326 326 | if False:
|
||||
327 327 | return 1
|
||||
328 328 | x = 2 \
|
||||
329 |+
|
||||
330 |+ return None
|
||||
328 328 | return 1
|
||||
329 329 | x = 2 \
|
||||
330 330 |
|
||||
331 |+ return None
|
||||
331 332 |
|
||||
332 333 |
|
||||
333 334 | # function return type annotation NoReturn
|
||||
|
||||
RET503.py:339:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
337 | if x == 5:
|
||||
338 | return 5
|
||||
339 | bar()
|
||||
| ^^^^^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
337 337 | if x == 5:
|
||||
338 338 | return 5
|
||||
339 339 | bar()
|
||||
340 |+ return None
|
||||
340 341 |
|
||||
341 342 |
|
||||
342 343 | def foo(string: str) -> str:
|
||||
|
||||
RET503.py:354:13: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
352 | return "third"
|
||||
353 | case _:
|
||||
354 | raises(string)
|
||||
| ^^^^^^^^^^^^^^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
352 352 | return "third"
|
||||
353 353 | case _:
|
||||
354 354 | raises(string)
|
||||
355 |+ return None
|
||||
355 356 |
|
||||
356 357 |
|
||||
357 358 | def foo() -> int:
|
||||
|
||||
RET503.py:370:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
||||
|
|
||||
368 | if baz() > 3:
|
||||
369 | return 1
|
||||
370 | bar()
|
||||
| ^^^^^ RET503
|
||||
|
|
||||
= help: Add explicit `return` statement
|
||||
|
||||
ℹ Unsafe fix
|
||||
368 368 | if baz() > 3:
|
||||
369 369 | return 1
|
||||
370 370 | bar()
|
||||
371 |+ return None
|
||||
|
||||
|
||||
|
||||
@@ -217,5 +217,28 @@ RET504.py:365:12: RET504 [*] Unnecessary assignment to `D` before `return` state
|
||||
364 |- D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
|
||||
365 |- return D
|
||||
364 |+ return 0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
|
||||
366 365 |
|
||||
367 366 |
|
||||
368 367 | # contextlib suppress in with statement
|
||||
|
||||
RET504.py:400:12: RET504 [*] Unnecessary assignment to `y` before `return` statement
|
||||
|
|
||||
398 | x = 1
|
||||
399 | y = y + 2
|
||||
400 | return y # RET504
|
||||
| ^ RET504
|
||||
|
|
||||
= help: Remove unnecessary assignment
|
||||
|
||||
ℹ Unsafe fix
|
||||
396 396 | x = 2
|
||||
397 397 | with contextlib.suppress(Exception):
|
||||
398 398 | x = 1
|
||||
399 |- y = y + 2
|
||||
400 |- return y # RET504
|
||||
399 |+ return y + 2
|
||||
401 400 |
|
||||
402 401 |
|
||||
403 402 | def foo():
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ RET505.py:8:5: RET505 Unnecessary `elif` after `return` statement
|
||||
9 | b = 2
|
||||
10 | return w
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
RET505.py:23:5: RET505 Unnecessary `elif` after `return` statement
|
||||
|
|
||||
@@ -20,6 +21,7 @@ RET505.py:23:5: RET505 Unnecessary `elif` after `return` statement
|
||||
24 | c = 2
|
||||
25 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
RET505.py:41:5: RET505 Unnecessary `elif` after `return` statement
|
||||
|
|
||||
@@ -30,6 +32,7 @@ RET505.py:41:5: RET505 Unnecessary `elif` after `return` statement
|
||||
42 | b = 2
|
||||
43 | return w
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
RET505.py:53:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
@@ -40,6 +43,7 @@ RET505.py:53:5: RET505 Unnecessary `else` after `return` statement
|
||||
54 | b = 2
|
||||
55 | return z
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:64:9: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
@@ -50,6 +54,7 @@ RET505.py:64:9: RET505 Unnecessary `else` after `return` statement
|
||||
65 | c = 3
|
||||
66 | return x
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:79:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
@@ -60,6 +65,7 @@ RET505.py:79:5: RET505 Unnecessary `else` after `return` statement
|
||||
80 | c = 3
|
||||
81 | return
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:89:9: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
@@ -70,6 +76,7 @@ RET505.py:89:9: RET505 Unnecessary `else` after `return` statement
|
||||
90 | b = 2
|
||||
91 | else:
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:99:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
@@ -80,5 +87,68 @@ RET505.py:99:5: RET505 Unnecessary `else` after `return` statement
|
||||
100 | try:
|
||||
101 | return False
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:137:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
135 | if True:
|
||||
136 | return
|
||||
137 | else:
|
||||
| ^^^^ RET505
|
||||
138 | # comment
|
||||
139 | pass
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:145:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
143 | if True:
|
||||
144 | return
|
||||
145 | else: # comment
|
||||
| ^^^^ RET505
|
||||
146 | pass
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:152:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
150 | if True:
|
||||
151 | return
|
||||
152 | else\
|
||||
| ^^^^ RET505
|
||||
153 | :\
|
||||
154 | # comment
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:161:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
159 | if True:
|
||||
160 | return
|
||||
161 | else\
|
||||
| ^^^^ RET505
|
||||
162 | : # comment
|
||||
163 | pass
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:169:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
167 | if True:
|
||||
168 | return
|
||||
169 | else: pass
|
||||
| ^^^^ RET505
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:175:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
173 | if True:
|
||||
174 | return
|
||||
175 | else:\
|
||||
| ^^^^ RET505
|
||||
176 | pass
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ RET506.py:8:5: RET506 Unnecessary `elif` after `raise` statement
|
||||
9 | b = 2
|
||||
10 | raise Exception(w)
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
RET506.py:23:5: RET506 Unnecessary `elif` after `raise` statement
|
||||
|
|
||||
@@ -20,6 +21,7 @@ RET506.py:23:5: RET506 Unnecessary `elif` after `raise` statement
|
||||
24 | raise Exception(y)
|
||||
25 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
RET506.py:34:5: RET506 Unnecessary `else` after `raise` statement
|
||||
|
|
||||
@@ -30,6 +32,7 @@ RET506.py:34:5: RET506 Unnecessary `else` after `raise` statement
|
||||
35 | b = 2
|
||||
36 | raise Exception(z)
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET506.py:45:9: RET506 Unnecessary `else` after `raise` statement
|
||||
|
|
||||
@@ -40,6 +43,7 @@ RET506.py:45:9: RET506 Unnecessary `else` after `raise` statement
|
||||
46 | c = 3
|
||||
47 | raise Exception(x)
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET506.py:60:5: RET506 Unnecessary `else` after `raise` statement
|
||||
|
|
||||
@@ -50,6 +54,7 @@ RET506.py:60:5: RET506 Unnecessary `else` after `raise` statement
|
||||
61 | c = 3
|
||||
62 | raise Exception(y)
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET506.py:70:9: RET506 Unnecessary `else` after `raise` statement
|
||||
|
|
||||
@@ -60,6 +65,7 @@ RET506.py:70:9: RET506 Unnecessary `else` after `raise` statement
|
||||
71 | b = 2
|
||||
72 | else:
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET506.py:80:5: RET506 Unnecessary `else` after `raise` statement
|
||||
|
|
||||
@@ -70,5 +76,6 @@ RET506.py:80:5: RET506 Unnecessary `else` after `raise` statement
|
||||
81 | try:
|
||||
82 | raise Exception(False)
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ RET507.py:8:9: RET507 Unnecessary `elif` after `continue` statement
|
||||
9 | continue
|
||||
10 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
RET507.py:22:9: RET507 Unnecessary `elif` after `continue` statement
|
||||
|
|
||||
@@ -20,6 +21,7 @@ RET507.py:22:9: RET507 Unnecessary `elif` after `continue` statement
|
||||
23 | c = 2
|
||||
24 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
RET507.py:36:9: RET507 Unnecessary `else` after `continue` statement
|
||||
|
|
||||
@@ -29,6 +31,7 @@ RET507.py:36:9: RET507 Unnecessary `else` after `continue` statement
|
||||
| ^^^^ RET507
|
||||
37 | a = z
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET507.py:47:13: RET507 Unnecessary `else` after `continue` statement
|
||||
|
|
||||
@@ -39,6 +42,7 @@ RET507.py:47:13: RET507 Unnecessary `else` after `continue` statement
|
||||
48 | c = 3
|
||||
49 | continue
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET507.py:63:9: RET507 Unnecessary `else` after `continue` statement
|
||||
|
|
||||
@@ -49,6 +53,7 @@ RET507.py:63:9: RET507 Unnecessary `else` after `continue` statement
|
||||
64 | c = 3
|
||||
65 | continue
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET507.py:74:13: RET507 Unnecessary `else` after `continue` statement
|
||||
|
|
||||
@@ -59,6 +64,7 @@ RET507.py:74:13: RET507 Unnecessary `else` after `continue` statement
|
||||
75 | b = 2
|
||||
76 | else:
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET507.py:85:9: RET507 Unnecessary `else` after `continue` statement
|
||||
|
|
||||
@@ -69,5 +75,6 @@ RET507.py:85:9: RET507 Unnecessary `else` after `continue` statement
|
||||
86 | try:
|
||||
87 | return
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ RET508.py:8:9: RET508 Unnecessary `elif` after `break` statement
|
||||
9 | break
|
||||
10 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
RET508.py:22:9: RET508 Unnecessary `elif` after `break` statement
|
||||
|
|
||||
@@ -20,6 +21,7 @@ RET508.py:22:9: RET508 Unnecessary `elif` after `break` statement
|
||||
23 | c = 2
|
||||
24 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
RET508.py:33:9: RET508 Unnecessary `else` after `break` statement
|
||||
|
|
||||
@@ -29,6 +31,7 @@ RET508.py:33:9: RET508 Unnecessary `else` after `break` statement
|
||||
| ^^^^ RET508
|
||||
34 | a = z
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET508.py:44:13: RET508 Unnecessary `else` after `break` statement
|
||||
|
|
||||
@@ -39,6 +42,7 @@ RET508.py:44:13: RET508 Unnecessary `else` after `break` statement
|
||||
45 | c = 3
|
||||
46 | break
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET508.py:60:9: RET508 Unnecessary `else` after `break` statement
|
||||
|
|
||||
@@ -49,6 +53,7 @@ RET508.py:60:9: RET508 Unnecessary `else` after `break` statement
|
||||
61 | c = 3
|
||||
62 | break
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET508.py:71:13: RET508 Unnecessary `else` after `break` statement
|
||||
|
|
||||
@@ -59,6 +64,7 @@ RET508.py:71:13: RET508 Unnecessary `else` after `break` statement
|
||||
72 | b = 2
|
||||
73 | else:
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET508.py:82:9: RET508 Unnecessary `else` after `break` statement
|
||||
|
|
||||
@@ -69,6 +75,7 @@ RET508.py:82:9: RET508 Unnecessary `else` after `break` statement
|
||||
83 | try:
|
||||
84 | return
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET508.py:158:13: RET508 Unnecessary `else` after `break` statement
|
||||
|
|
||||
@@ -78,5 +85,6 @@ RET508.py:158:13: RET508 Unnecessary `else` after `break` statement
|
||||
| ^^^^ RET508
|
||||
159 | a = z
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_return/mod.rs
|
||||
---
|
||||
RET505.py:8:5: RET505 [*] Unnecessary `elif` after `return` statement
|
||||
|
|
||||
6 | a = 1
|
||||
7 | return y
|
||||
8 | elif z:
|
||||
| ^^^^ RET505
|
||||
9 | b = 2
|
||||
10 | return w
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | if x: # [no-else-return]
|
||||
6 6 | a = 1
|
||||
7 7 | return y
|
||||
8 |- elif z:
|
||||
8 |+ if z:
|
||||
9 9 | b = 2
|
||||
10 10 | return w
|
||||
11 11 | else:
|
||||
|
||||
RET505.py:23:5: RET505 [*] Unnecessary `elif` after `return` statement
|
||||
|
|
||||
21 | b = 2
|
||||
22 | return
|
||||
23 | elif z:
|
||||
| ^^^^ RET505
|
||||
24 | c = 2
|
||||
25 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
20 20 | else:
|
||||
21 21 | b = 2
|
||||
22 22 | return
|
||||
23 |- elif z:
|
||||
23 |+ if z:
|
||||
24 24 | c = 2
|
||||
25 25 | else:
|
||||
26 26 | c = 3
|
||||
|
||||
RET505.py:41:5: RET505 [*] Unnecessary `elif` after `return` statement
|
||||
|
|
||||
39 | a = 1
|
||||
40 | return y
|
||||
41 | elif z:
|
||||
| ^^^^ RET505
|
||||
42 | b = 2
|
||||
43 | return w
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
38 38 | if x: # [no-else-return]
|
||||
39 39 | a = 1
|
||||
40 40 | return y
|
||||
41 |- elif z:
|
||||
41 |+ if z:
|
||||
42 42 | b = 2
|
||||
43 43 | return w
|
||||
44 44 | else:
|
||||
|
||||
RET505.py:53:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
51 | a = 1
|
||||
52 | return y
|
||||
53 | else:
|
||||
| ^^^^ RET505
|
||||
54 | b = 2
|
||||
55 | return z
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
50 50 | if x: # [no-else-return]
|
||||
51 51 | a = 1
|
||||
52 52 | return y
|
||||
53 |- else:
|
||||
54 |- b = 2
|
||||
55 |- return z
|
||||
53 |+ b = 2
|
||||
54 |+ return z
|
||||
56 55 |
|
||||
57 56 |
|
||||
58 57 | def foo3(x, y, z):
|
||||
|
||||
RET505.py:64:9: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
62 | b = 2
|
||||
63 | return y
|
||||
64 | else:
|
||||
| ^^^^ RET505
|
||||
65 | c = 3
|
||||
66 | return x
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | if y: # [no-else-return]
|
||||
62 62 | b = 2
|
||||
63 63 | return y
|
||||
64 |- else:
|
||||
65 |- c = 3
|
||||
66 |- return x
|
||||
64 |+ c = 3
|
||||
65 |+ return x
|
||||
67 66 | else:
|
||||
68 67 | d = 4
|
||||
69 68 | return z
|
||||
|
||||
RET505.py:79:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
77 | b = 2
|
||||
78 | return
|
||||
79 | else:
|
||||
| ^^^^ RET505
|
||||
80 | c = 3
|
||||
81 | return
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
76 76 | else:
|
||||
77 77 | b = 2
|
||||
78 78 | return
|
||||
79 |- else:
|
||||
80 |- c = 3
|
||||
79 |+ c = 3
|
||||
81 80 | return
|
||||
82 81 |
|
||||
83 82 |
|
||||
|
||||
RET505.py:89:9: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
87 | a = 4
|
||||
88 | return
|
||||
89 | else:
|
||||
| ^^^^ RET505
|
||||
90 | b = 2
|
||||
91 | else:
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
86 86 | if y: # [no-else-return]
|
||||
87 87 | a = 4
|
||||
88 88 | return
|
||||
89 |- else:
|
||||
90 |- b = 2
|
||||
89 |+ b = 2
|
||||
91 90 | else:
|
||||
92 91 | c = 3
|
||||
93 92 | return
|
||||
|
||||
RET505.py:99:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
97 | if x: # [no-else-return]
|
||||
98 | return True
|
||||
99 | else:
|
||||
| ^^^^ RET505
|
||||
100 | try:
|
||||
101 | return False
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
96 96 | def bar4(x):
|
||||
97 97 | if x: # [no-else-return]
|
||||
98 98 | return True
|
||||
99 |- else:
|
||||
100 |- try:
|
||||
101 |- return False
|
||||
102 |- except ValueError:
|
||||
103 |- return None
|
||||
99 |+ try:
|
||||
100 |+ return False
|
||||
101 |+ except ValueError:
|
||||
102 |+ return None
|
||||
104 103 |
|
||||
105 104 |
|
||||
106 105 | ###
|
||||
|
||||
RET505.py:137:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
135 | if True:
|
||||
136 | return
|
||||
137 | else:
|
||||
| ^^^^ RET505
|
||||
138 | # comment
|
||||
139 | pass
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
134 134 | def bar4(x):
|
||||
135 135 | if True:
|
||||
136 136 | return
|
||||
137 |- else:
|
||||
138 |- # comment
|
||||
139 |- pass
|
||||
137 |+ # comment
|
||||
138 |+ pass
|
||||
140 139 |
|
||||
141 140 |
|
||||
142 141 | def bar5():
|
||||
|
||||
RET505.py:145:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
143 | if True:
|
||||
144 | return
|
||||
145 | else: # comment
|
||||
| ^^^^ RET505
|
||||
146 | pass
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
142 142 | def bar5():
|
||||
143 143 | if True:
|
||||
144 144 | return
|
||||
145 |- else: # comment
|
||||
146 |- pass
|
||||
145 |+ # comment
|
||||
146 |+ pass
|
||||
147 147 |
|
||||
148 148 |
|
||||
149 149 | def bar6():
|
||||
|
||||
RET505.py:152:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
150 | if True:
|
||||
151 | return
|
||||
152 | else\
|
||||
| ^^^^ RET505
|
||||
153 | :\
|
||||
154 | # comment
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
149 149 | def bar6():
|
||||
150 150 | if True:
|
||||
151 151 | return
|
||||
152 |- else\
|
||||
153 |- :\
|
||||
154 |- # comment
|
||||
155 |- pass
|
||||
152 |+ # comment
|
||||
153 |+ pass
|
||||
156 154 |
|
||||
157 155 |
|
||||
158 156 | def bar7():
|
||||
|
||||
RET505.py:161:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
159 | if True:
|
||||
160 | return
|
||||
161 | else\
|
||||
| ^^^^ RET505
|
||||
162 | : # comment
|
||||
163 | pass
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
158 158 | def bar7():
|
||||
159 159 | if True:
|
||||
160 160 | return
|
||||
161 |- else\
|
||||
162 |- : # comment
|
||||
163 |- pass
|
||||
161 |+ # comment
|
||||
162 |+ pass
|
||||
164 163 |
|
||||
165 164 |
|
||||
166 165 | def bar8():
|
||||
|
||||
RET505.py:169:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
167 | if True:
|
||||
168 | return
|
||||
169 | else: pass
|
||||
| ^^^^ RET505
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
166 166 | def bar8():
|
||||
167 167 | if True:
|
||||
168 168 | return
|
||||
169 |- else: pass
|
||||
169 |+ pass
|
||||
170 170 |
|
||||
171 171 |
|
||||
172 172 | def bar9():
|
||||
|
||||
RET505.py:175:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
173 | if True:
|
||||
174 | return
|
||||
175 | else:\
|
||||
| ^^^^ RET505
|
||||
176 | pass
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
172 172 | def bar9():
|
||||
173 173 | if True:
|
||||
174 174 | return
|
||||
175 |- else:\
|
||||
176 |- pass
|
||||
175 |+ pass
|
||||
177 176 |
|
||||
178 177 |
|
||||
179 178 | x = 0
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_return/mod.rs
|
||||
---
|
||||
RET506.py:8:5: RET506 [*] Unnecessary `elif` after `raise` statement
|
||||
|
|
||||
6 | a = 1
|
||||
7 | raise Exception(y)
|
||||
8 | elif z:
|
||||
| ^^^^ RET506
|
||||
9 | b = 2
|
||||
10 | raise Exception(w)
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | if x: # [no-else-raise]
|
||||
6 6 | a = 1
|
||||
7 7 | raise Exception(y)
|
||||
8 |- elif z:
|
||||
8 |+ if z:
|
||||
9 9 | b = 2
|
||||
10 10 | raise Exception(w)
|
||||
11 11 | else:
|
||||
|
||||
RET506.py:23:5: RET506 [*] Unnecessary `elif` after `raise` statement
|
||||
|
|
||||
21 | b = 2
|
||||
22 | raise Exception(x)
|
||||
23 | elif z:
|
||||
| ^^^^ RET506
|
||||
24 | raise Exception(y)
|
||||
25 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
20 20 | else:
|
||||
21 21 | b = 2
|
||||
22 22 | raise Exception(x)
|
||||
23 |- elif z:
|
||||
23 |+ if z:
|
||||
24 24 | raise Exception(y)
|
||||
25 25 | else:
|
||||
26 26 | c = 3
|
||||
|
||||
RET506.py:34:5: RET506 [*] Unnecessary `else` after `raise` statement
|
||||
|
|
||||
32 | a = 1
|
||||
33 | raise Exception(y)
|
||||
34 | else:
|
||||
| ^^^^ RET506
|
||||
35 | b = 2
|
||||
36 | raise Exception(z)
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
31 31 | if x: # [no-else-raise]
|
||||
32 32 | a = 1
|
||||
33 33 | raise Exception(y)
|
||||
34 |- else:
|
||||
35 |- b = 2
|
||||
36 |- raise Exception(z)
|
||||
34 |+ b = 2
|
||||
35 |+ raise Exception(z)
|
||||
37 36 |
|
||||
38 37 |
|
||||
39 38 | def foo3(x, y, z):
|
||||
|
||||
RET506.py:45:9: RET506 [*] Unnecessary `else` after `raise` statement
|
||||
|
|
||||
43 | b = 2
|
||||
44 | raise Exception(y)
|
||||
45 | else:
|
||||
| ^^^^ RET506
|
||||
46 | c = 3
|
||||
47 | raise Exception(x)
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
42 42 | if y: # [no-else-raise]
|
||||
43 43 | b = 2
|
||||
44 44 | raise Exception(y)
|
||||
45 |- else:
|
||||
46 |- c = 3
|
||||
47 |- raise Exception(x)
|
||||
45 |+ c = 3
|
||||
46 |+ raise Exception(x)
|
||||
48 47 | else:
|
||||
49 48 | d = 4
|
||||
50 49 | raise Exception(z)
|
||||
|
||||
RET506.py:60:5: RET506 [*] Unnecessary `else` after `raise` statement
|
||||
|
|
||||
58 | b = 2
|
||||
59 | raise Exception(x)
|
||||
60 | else:
|
||||
| ^^^^ RET506
|
||||
61 | c = 3
|
||||
62 | raise Exception(y)
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
57 57 | else:
|
||||
58 58 | b = 2
|
||||
59 59 | raise Exception(x)
|
||||
60 |- else:
|
||||
61 |- c = 3
|
||||
60 |+ c = 3
|
||||
62 61 | raise Exception(y)
|
||||
63 62 |
|
||||
64 63 |
|
||||
|
||||
RET506.py:70:9: RET506 [*] Unnecessary `else` after `raise` statement
|
||||
|
|
||||
68 | a = 4
|
||||
69 | raise Exception(x)
|
||||
70 | else:
|
||||
| ^^^^ RET506
|
||||
71 | b = 2
|
||||
72 | else:
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
67 67 | if y: # [no-else-raise]
|
||||
68 68 | a = 4
|
||||
69 69 | raise Exception(x)
|
||||
70 |- else:
|
||||
71 |- b = 2
|
||||
70 |+ b = 2
|
||||
72 71 | else:
|
||||
73 72 | c = 3
|
||||
74 73 | raise Exception(y)
|
||||
|
||||
RET506.py:80:5: RET506 [*] Unnecessary `else` after `raise` statement
|
||||
|
|
||||
78 | if x: # [no-else-raise]
|
||||
79 | raise Exception(True)
|
||||
80 | else:
|
||||
| ^^^^ RET506
|
||||
81 | try:
|
||||
82 | raise Exception(False)
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
77 77 | def bar4(x):
|
||||
78 78 | if x: # [no-else-raise]
|
||||
79 79 | raise Exception(True)
|
||||
80 |- else:
|
||||
81 |- try:
|
||||
82 |- raise Exception(False)
|
||||
83 |- except ValueError:
|
||||
84 |- raise Exception(None)
|
||||
80 |+ try:
|
||||
81 |+ raise Exception(False)
|
||||
82 |+ except ValueError:
|
||||
83 |+ raise Exception(None)
|
||||
85 84 |
|
||||
86 85 |
|
||||
87 86 | ###
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_return/mod.rs
|
||||
---
|
||||
RET507.py:8:9: RET507 [*] Unnecessary `elif` after `continue` statement
|
||||
|
|
||||
6 | if i < y: # [no-else-continue]
|
||||
7 | continue
|
||||
8 | elif i < w:
|
||||
| ^^^^ RET507
|
||||
9 | continue
|
||||
10 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | for i in x:
|
||||
6 6 | if i < y: # [no-else-continue]
|
||||
7 7 | continue
|
||||
8 |- elif i < w:
|
||||
8 |+ if i < w:
|
||||
9 9 | continue
|
||||
10 10 | else:
|
||||
11 11 | a = z
|
||||
|
||||
RET507.py:22:9: RET507 [*] Unnecessary `elif` after `continue` statement
|
||||
|
|
||||
20 | b = 2
|
||||
21 | continue
|
||||
22 | elif z:
|
||||
| ^^^^ RET507
|
||||
23 | c = 2
|
||||
24 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
19 19 | else:
|
||||
20 20 | b = 2
|
||||
21 21 | continue
|
||||
22 |- elif z:
|
||||
22 |+ if z:
|
||||
23 23 | c = 2
|
||||
24 24 | else:
|
||||
25 25 | c = 3
|
||||
|
||||
RET507.py:36:9: RET507 [*] Unnecessary `else` after `continue` statement
|
||||
|
|
||||
34 | if i < y: # [no-else-continue]
|
||||
35 | continue
|
||||
36 | else:
|
||||
| ^^^^ RET507
|
||||
37 | a = z
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
33 33 | for i in x:
|
||||
34 34 | if i < y: # [no-else-continue]
|
||||
35 35 | continue
|
||||
36 |- else:
|
||||
37 |- a = z
|
||||
36 |+ a = z
|
||||
38 37 |
|
||||
39 38 |
|
||||
40 39 | def foo3(x, y, z):
|
||||
|
||||
RET507.py:47:13: RET507 [*] Unnecessary `else` after `continue` statement
|
||||
|
|
||||
45 | b = 2
|
||||
46 | continue
|
||||
47 | else:
|
||||
| ^^^^ RET507
|
||||
48 | c = 3
|
||||
49 | continue
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
44 44 | if z: # [no-else-continue]
|
||||
45 45 | b = 2
|
||||
46 46 | continue
|
||||
47 |- else:
|
||||
48 |- c = 3
|
||||
49 |- continue
|
||||
47 |+ c = 3
|
||||
48 |+ continue
|
||||
50 49 | else:
|
||||
51 50 | d = 4
|
||||
52 51 | continue
|
||||
|
||||
RET507.py:63:9: RET507 [*] Unnecessary `else` after `continue` statement
|
||||
|
|
||||
61 | b = 2
|
||||
62 | continue
|
||||
63 | else:
|
||||
| ^^^^ RET507
|
||||
64 | c = 3
|
||||
65 | continue
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
60 60 | else:
|
||||
61 61 | b = 2
|
||||
62 62 | continue
|
||||
63 |- else:
|
||||
64 |- c = 3
|
||||
63 |+ c = 3
|
||||
65 64 | continue
|
||||
66 65 |
|
||||
67 66 |
|
||||
|
||||
RET507.py:74:13: RET507 [*] Unnecessary `else` after `continue` statement
|
||||
|
|
||||
72 | a = 4
|
||||
73 | continue
|
||||
74 | else:
|
||||
| ^^^^ RET507
|
||||
75 | b = 2
|
||||
76 | else:
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
71 71 | if y: # [no-else-continue]
|
||||
72 72 | a = 4
|
||||
73 73 | continue
|
||||
74 |- else:
|
||||
75 |- b = 2
|
||||
74 |+ b = 2
|
||||
76 75 | else:
|
||||
77 76 | c = 3
|
||||
78 77 | continue
|
||||
|
||||
RET507.py:85:9: RET507 [*] Unnecessary `else` after `continue` statement
|
||||
|
|
||||
83 | if x: # [no-else-continue]
|
||||
84 | continue
|
||||
85 | else:
|
||||
| ^^^^ RET507
|
||||
86 | try:
|
||||
87 | return
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
82 82 | for i in range(10):
|
||||
83 83 | if x: # [no-else-continue]
|
||||
84 84 | continue
|
||||
85 |- else:
|
||||
86 |- try:
|
||||
87 |- return
|
||||
88 |- except ValueError:
|
||||
89 |- continue
|
||||
85 |+ try:
|
||||
86 |+ return
|
||||
87 |+ except ValueError:
|
||||
88 |+ continue
|
||||
90 89 |
|
||||
91 90 |
|
||||
92 91 | def bar1(x, y, z):
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_return/mod.rs
|
||||
---
|
||||
RET508.py:8:9: RET508 [*] Unnecessary `elif` after `break` statement
|
||||
|
|
||||
6 | if i > y: # [no-else-break]
|
||||
7 | break
|
||||
8 | elif i > w:
|
||||
| ^^^^ RET508
|
||||
9 | break
|
||||
10 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | for i in x:
|
||||
6 6 | if i > y: # [no-else-break]
|
||||
7 7 | break
|
||||
8 |- elif i > w:
|
||||
8 |+ if i > w:
|
||||
9 9 | break
|
||||
10 10 | else:
|
||||
11 11 | a = z
|
||||
|
||||
RET508.py:22:9: RET508 [*] Unnecessary `elif` after `break` statement
|
||||
|
|
||||
20 | b = 2
|
||||
21 | break
|
||||
22 | elif z:
|
||||
| ^^^^ RET508
|
||||
23 | c = 2
|
||||
24 | else:
|
||||
|
|
||||
= help: Remove unnecessary `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
19 19 | else:
|
||||
20 20 | b = 2
|
||||
21 21 | break
|
||||
22 |- elif z:
|
||||
22 |+ if z:
|
||||
23 23 | c = 2
|
||||
24 24 | else:
|
||||
25 25 | c = 3
|
||||
|
||||
RET508.py:33:9: RET508 [*] Unnecessary `else` after `break` statement
|
||||
|
|
||||
31 | if i > y: # [no-else-break]
|
||||
32 | break
|
||||
33 | else:
|
||||
| ^^^^ RET508
|
||||
34 | a = z
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
30 30 | for i in x:
|
||||
31 31 | if i > y: # [no-else-break]
|
||||
32 32 | break
|
||||
33 |- else:
|
||||
34 |- a = z
|
||||
33 |+ a = z
|
||||
35 34 |
|
||||
36 35 |
|
||||
37 36 | def foo3(x, y, z):
|
||||
|
||||
RET508.py:44:13: RET508 [*] Unnecessary `else` after `break` statement
|
||||
|
|
||||
42 | b = 2
|
||||
43 | break
|
||||
44 | else:
|
||||
| ^^^^ RET508
|
||||
45 | c = 3
|
||||
46 | break
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
41 41 | if z: # [no-else-break]
|
||||
42 42 | b = 2
|
||||
43 43 | break
|
||||
44 |- else:
|
||||
45 |- c = 3
|
||||
46 |- break
|
||||
44 |+ c = 3
|
||||
45 |+ break
|
||||
47 46 | else:
|
||||
48 47 | d = 4
|
||||
49 48 | break
|
||||
|
||||
RET508.py:60:9: RET508 [*] Unnecessary `else` after `break` statement
|
||||
|
|
||||
58 | b = 2
|
||||
59 | break
|
||||
60 | else:
|
||||
| ^^^^ RET508
|
||||
61 | c = 3
|
||||
62 | break
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
57 57 | else:
|
||||
58 58 | b = 2
|
||||
59 59 | break
|
||||
60 |- else:
|
||||
61 |- c = 3
|
||||
60 |+ c = 3
|
||||
62 61 | break
|
||||
63 62 |
|
||||
64 63 |
|
||||
|
||||
RET508.py:71:13: RET508 [*] Unnecessary `else` after `break` statement
|
||||
|
|
||||
69 | a = 4
|
||||
70 | break
|
||||
71 | else:
|
||||
| ^^^^ RET508
|
||||
72 | b = 2
|
||||
73 | else:
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
68 68 | if y: # [no-else-break]
|
||||
69 69 | a = 4
|
||||
70 70 | break
|
||||
71 |- else:
|
||||
72 |- b = 2
|
||||
71 |+ b = 2
|
||||
73 72 | else:
|
||||
74 73 | c = 3
|
||||
75 74 | break
|
||||
|
||||
RET508.py:82:9: RET508 [*] Unnecessary `else` after `break` statement
|
||||
|
|
||||
80 | if x: # [no-else-break]
|
||||
81 | break
|
||||
82 | else:
|
||||
| ^^^^ RET508
|
||||
83 | try:
|
||||
84 | return
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
79 79 | for i in range(10):
|
||||
80 80 | if x: # [no-else-break]
|
||||
81 81 | break
|
||||
82 |- else:
|
||||
83 |- try:
|
||||
84 |- return
|
||||
85 |- except ValueError:
|
||||
86 |- break
|
||||
82 |+ try:
|
||||
83 |+ return
|
||||
84 |+ except ValueError:
|
||||
85 |+ break
|
||||
87 86 |
|
||||
88 87 |
|
||||
89 88 | ###
|
||||
|
||||
RET508.py:158:13: RET508 [*] Unnecessary `else` after `break` statement
|
||||
|
|
||||
156 | if i > w:
|
||||
157 | break
|
||||
158 | else:
|
||||
| ^^^^ RET508
|
||||
159 | a = z
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
155 155 | else:
|
||||
156 156 | if i > w:
|
||||
157 157 | break
|
||||
158 |- else:
|
||||
159 |- a = z
|
||||
158 |+ a = z
|
||||
|
||||
|
||||
@@ -3,34 +3,48 @@ use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_python_ast::visitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Stack<'a> {
|
||||
pub(super) struct Stack<'data> {
|
||||
/// The `return` statements in the current function.
|
||||
pub(super) returns: Vec<&'a ast::StmtReturn>,
|
||||
pub(super) returns: Vec<&'data ast::StmtReturn>,
|
||||
/// The `elif` or `else` statements in the current function.
|
||||
pub(super) elifs_elses: Vec<(&'a [Stmt], &'a ElifElseClause)>,
|
||||
pub(super) elifs_elses: Vec<(&'data [Stmt], &'data ElifElseClause)>,
|
||||
/// The non-local variables in the current function.
|
||||
pub(super) non_locals: FxHashSet<&'a str>,
|
||||
pub(super) non_locals: FxHashSet<&'data str>,
|
||||
/// Whether the current function is a generator.
|
||||
pub(super) is_generator: bool,
|
||||
/// The `assignment`-to-`return` statement pairs in the current function.
|
||||
/// TODO(charlie): Remove the extra [`Stmt`] here, which is necessary to support statement
|
||||
/// removal for the `return` statement.
|
||||
pub(super) assignment_return: Vec<(&'a ast::StmtAssign, &'a ast::StmtReturn, &'a Stmt)>,
|
||||
pub(super) assignment_return:
|
||||
Vec<(&'data ast::StmtAssign, &'data ast::StmtReturn, &'data Stmt)>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct ReturnVisitor<'a> {
|
||||
pub(super) struct ReturnVisitor<'semantic, 'data> {
|
||||
/// The semantic model of the current file.
|
||||
semantic: &'semantic SemanticModel<'data>,
|
||||
/// The current stack of nodes.
|
||||
pub(super) stack: Stack<'a>,
|
||||
pub(super) stack: Stack<'data>,
|
||||
/// The preceding sibling of the current node.
|
||||
sibling: Option<&'a Stmt>,
|
||||
sibling: Option<&'data Stmt>,
|
||||
/// The parent nodes of the current node.
|
||||
parents: Vec<&'a Stmt>,
|
||||
parents: Vec<&'data Stmt>,
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for ReturnVisitor<'a> {
|
||||
impl<'semantic, 'data> ReturnVisitor<'semantic, 'data> {
|
||||
pub(super) fn new(semantic: &'semantic SemanticModel<'data>) -> Self {
|
||||
Self {
|
||||
semantic,
|
||||
stack: Stack::default(),
|
||||
sibling: None,
|
||||
parents: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'semantic, 'a> Visitor<'a> for ReturnVisitor<'semantic, 'a> {
|
||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||
match stmt {
|
||||
Stmt::ClassDef(ast::StmtClassDef { decorator_list, .. }) => {
|
||||
@@ -95,11 +109,17 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
|
||||
// x = f.read()
|
||||
// return x
|
||||
// ```
|
||||
Stmt::With(ast::StmtWith { body, .. }) => {
|
||||
if let Some(stmt_assign) = body.last().and_then(Stmt::as_assign_stmt) {
|
||||
self.stack
|
||||
.assignment_return
|
||||
.push((stmt_assign, stmt_return, stmt));
|
||||
Stmt::With(with) => {
|
||||
if let Some(stmt_assign) =
|
||||
with.body.last().and_then(Stmt::as_assign_stmt)
|
||||
{
|
||||
if !has_conditional_body(with, self.semantic) {
|
||||
self.stack.assignment_return.push((
|
||||
stmt_assign,
|
||||
stmt_return,
|
||||
stmt,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -142,3 +162,47 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
|
||||
self.sibling = sibling;
|
||||
}
|
||||
}
|
||||
|
||||
/// RET504
|
||||
/// If the last statement is a `return` statement, and the second-to-last statement is a
|
||||
/// `with` statement that suppresses an exception, then we should not analyze the `return`
|
||||
/// statement for unnecessary assignments. Otherwise we will suggest removing the assignment
|
||||
/// and the `with` statement, which would change the behavior of the code.
|
||||
///
|
||||
/// Example:
|
||||
/// ```python
|
||||
/// def foo(data):
|
||||
/// with suppress(JSONDecoderError):
|
||||
/// data = data.decode()
|
||||
/// return data
|
||||
|
||||
/// Returns `true` if the [`With`] statement is known to have a conditional body. In other words:
|
||||
/// if the [`With`] statement's body may or may not run.
|
||||
///
|
||||
/// For example, in the following, it's unsafe to inline the `return` into the `with`, since if
|
||||
/// `data.decode()` fails, the behavior of the program will differ. (As-is, the function will return
|
||||
/// the input `data`; if we inline the `return`, the function will return `None`.)
|
||||
///
|
||||
/// ```python
|
||||
/// def func(data):
|
||||
/// with suppress(JSONDecoderError):
|
||||
/// data = data.decode()
|
||||
/// return data
|
||||
/// ```
|
||||
fn has_conditional_body(with: &ast::StmtWith, semantic: &SemanticModel) -> bool {
|
||||
with.items.iter().any(|item| {
|
||||
let ast::WithItem {
|
||||
context_expr: Expr::Call(ast::ExprCall { func, .. }),
|
||||
..
|
||||
} = item
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if let Some(call_path) = semantic.resolve_call_path(func) {
|
||||
if call_path.as_slice() == ["contextlib", "suppress"] {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ mod tests {
|
||||
#[test_case(Rule::YodaConditions, Path::new("SIM300.py"))]
|
||||
#[test_case(Rule::IfElseBlockInsteadOfDictGet, Path::new("SIM401.py"))]
|
||||
#[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))]
|
||||
#[test_case(Rule::IfWithSameArms, Path::new("SIM114.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::comparable::ComparableStmt;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::stmt_if::{if_elif_branches, IfElifBranch};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
@@ -32,14 +39,20 @@ use crate::checkers::ast::Checker;
|
||||
pub struct IfWithSameArms;
|
||||
|
||||
impl Violation for IfWithSameArms {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Combine `if` branches using logical `or` operator")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Combine `if` branches".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// SIM114
|
||||
pub(crate) fn if_with_same_arms(checker: &mut Checker, locator: &Locator, stmt_if: &ast::StmtIf) {
|
||||
pub(crate) fn if_with_same_arms(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
let mut branches_iter = if_elif_branches(stmt_if).peekable();
|
||||
while let Some(current_branch) = branches_iter.next() {
|
||||
let Some(following_branch) = branches_iter.peek() else {
|
||||
@@ -63,26 +76,101 @@ pub(crate) fn if_with_same_arms(checker: &mut Checker, locator: &Locator, stmt_i
|
||||
let first_comments = checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.comments_in_range(body_range(¤t_branch, locator))
|
||||
.comments_in_range(body_range(¤t_branch, checker.locator()))
|
||||
.iter()
|
||||
.map(|range| locator.slice(*range));
|
||||
.map(|range| checker.locator().slice(*range));
|
||||
let second_comments = checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.comments_in_range(body_range(following_branch, locator))
|
||||
.comments_in_range(body_range(following_branch, checker.locator()))
|
||||
.iter()
|
||||
.map(|range| locator.slice(*range));
|
||||
.map(|range| checker.locator().slice(*range));
|
||||
if !first_comments.eq(second_comments) {
|
||||
continue;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
IfWithSameArms,
|
||||
TextRange::new(current_branch.start(), following_branch.end()),
|
||||
));
|
||||
);
|
||||
|
||||
if checker.settings.preview.is_enabled() {
|
||||
diagnostic.try_set_fix(|| {
|
||||
merge_branches(
|
||||
stmt_if,
|
||||
¤t_branch,
|
||||
following_branch,
|
||||
checker.locator(),
|
||||
checker.indexer(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a [`Fix`] to merge two [`IfElifBranch`] branches.
|
||||
fn merge_branches(
|
||||
stmt_if: &ast::StmtIf,
|
||||
current_branch: &IfElifBranch,
|
||||
following_branch: &IfElifBranch,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
) -> Result<Fix> {
|
||||
// Identify the colon (`:`) at the end of the current branch's test.
|
||||
let Some(current_branch_colon) =
|
||||
SimpleTokenizer::starts_at(current_branch.test.end(), locator.contents())
|
||||
.find(|token| token.kind == SimpleTokenKind::Colon)
|
||||
else {
|
||||
return Err(anyhow::anyhow!("Expected colon after test"));
|
||||
};
|
||||
|
||||
let mut following_branch_tokenizer =
|
||||
SimpleTokenizer::starts_at(following_branch.test.end(), locator.contents());
|
||||
|
||||
// Identify the colon (`:`) at the end of the following branch's test.
|
||||
let Some(following_branch_colon) =
|
||||
following_branch_tokenizer.find(|token| token.kind == SimpleTokenKind::Colon)
|
||||
else {
|
||||
return Err(anyhow::anyhow!("Expected colon after test"));
|
||||
};
|
||||
|
||||
let main_edit = Edit::deletion(
|
||||
locator.full_line_end(current_branch_colon.end()),
|
||||
locator.full_line_end(following_branch_colon.end()),
|
||||
);
|
||||
|
||||
// If the test isn't parenthesized, consider parenthesizing it.
|
||||
let following_branch_test = if let Some(range) = parenthesized_range(
|
||||
following_branch.test.into(),
|
||||
stmt_if.into(),
|
||||
indexer.comment_ranges(),
|
||||
locator.contents(),
|
||||
) {
|
||||
Cow::Borrowed(locator.slice(range))
|
||||
} else if matches!(
|
||||
following_branch.test,
|
||||
Expr::BoolOp(ast::ExprBoolOp {
|
||||
op: ast::BoolOp::Or,
|
||||
..
|
||||
}) | Expr::Lambda(_)
|
||||
| Expr::NamedExpr(_)
|
||||
) {
|
||||
Cow::Owned(format!("({})", locator.slice(following_branch.test)))
|
||||
} else {
|
||||
Cow::Borrowed(locator.slice(following_branch.test))
|
||||
};
|
||||
|
||||
Ok(Fix::safe_edits(
|
||||
main_edit,
|
||||
[Edit::insertion(
|
||||
format!(" or {following_branch_test}"),
|
||||
current_branch_colon.start(),
|
||||
)],
|
||||
))
|
||||
}
|
||||
|
||||
/// Return the [`TextRange`] of an [`IfElifBranch`]'s body (from the end of the test to the end of
|
||||
/// the body).
|
||||
fn body_range(branch: &IfElifBranch, locator: &Locator) -> TextRange {
|
||||
|
||||
@@ -5,6 +5,7 @@ use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checki
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `if` statements that can be replaced with `bool`.
|
||||
@@ -30,7 +31,8 @@ use crate::checkers::ast::Checker;
|
||||
/// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing)
|
||||
#[violation]
|
||||
pub struct NeedlessBool {
|
||||
condition: String,
|
||||
condition: SourceCodeSnippet,
|
||||
replacement: Option<SourceCodeSnippet>,
|
||||
}
|
||||
|
||||
impl Violation for NeedlessBool {
|
||||
@@ -38,13 +40,24 @@ impl Violation for NeedlessBool {
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let NeedlessBool { condition } = self;
|
||||
format!("Return the condition `{condition}` directly")
|
||||
let NeedlessBool { condition, .. } = self;
|
||||
if let Some(condition) = condition.full_display() {
|
||||
format!("Return the condition `{condition}` directly")
|
||||
} else {
|
||||
format!("Return the condition directly")
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let NeedlessBool { condition } = self;
|
||||
Some(format!("Replace with `return {condition}`"))
|
||||
let NeedlessBool { replacement, .. } = self;
|
||||
if let Some(replacement) = replacement
|
||||
.as_ref()
|
||||
.and_then(SourceCodeSnippet::full_display)
|
||||
{
|
||||
Some(format!("Replace with `{replacement}`"))
|
||||
} else {
|
||||
Some(format!("Inline condition"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,11 +103,20 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
return;
|
||||
};
|
||||
|
||||
// If the branches have the same condition, abort (although the code could be
|
||||
// simplified).
|
||||
if if_return == else_return {
|
||||
return;
|
||||
}
|
||||
// Determine whether the return values are inverted, as in:
|
||||
// ```python
|
||||
// if x > 0:
|
||||
// return False
|
||||
// else:
|
||||
// return True
|
||||
// ```
|
||||
let inverted = match (if_return, else_return) {
|
||||
(Bool::True, Bool::False) => false,
|
||||
(Bool::False, Bool::True) => true,
|
||||
// If the branches have the same condition, abort (although the code could be
|
||||
// simplified).
|
||||
_ => return,
|
||||
};
|
||||
|
||||
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
|
||||
if is_sys_version_block(stmt_if, checker.semantic()) {
|
||||
@@ -106,33 +128,38 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
return;
|
||||
}
|
||||
|
||||
let condition = checker.generator().expr(if_test);
|
||||
let mut diagnostic = Diagnostic::new(NeedlessBool { condition }, range);
|
||||
if matches!(if_return, Bool::True)
|
||||
&& matches!(else_return, Bool::False)
|
||||
&& !checker.indexer().has_comments(&range, checker.locator())
|
||||
&& (if_test.is_compare_expr() || checker.semantic().is_builtin("bool"))
|
||||
{
|
||||
if if_test.is_compare_expr() {
|
||||
// If the condition is a comparison, we can replace it with the condition.
|
||||
let condition = checker.locator().slice(if_test);
|
||||
let replacement = if checker.indexer().has_comments(&range, checker.locator()) {
|
||||
None
|
||||
} else {
|
||||
// If the return values are inverted, wrap the condition in a `not`.
|
||||
if inverted {
|
||||
let node = ast::StmtReturn {
|
||||
value: Some(Box::new(Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: ast::UnaryOp::Not,
|
||||
operand: Box::new(if_test.clone()),
|
||||
range: TextRange::default(),
|
||||
}))),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
Some(checker.generator().stmt(&node.into()))
|
||||
} else if if_test.is_compare_expr() {
|
||||
// If the condition is a comparison, we can replace it with the condition, since we
|
||||
// know it's a boolean.
|
||||
let node = ast::StmtReturn {
|
||||
value: Some(Box::new(if_test.clone())),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
checker.generator().stmt(&node.into()),
|
||||
range,
|
||||
)));
|
||||
} else {
|
||||
// Otherwise, we need to wrap the condition in a call to `bool`. (We've already
|
||||
// verified, above, that `bool` is a builtin.)
|
||||
let node = ast::ExprName {
|
||||
Some(checker.generator().stmt(&node.into()))
|
||||
} else if checker.semantic().is_builtin("bool") {
|
||||
// Otherwise, we need to wrap the condition in a call to `bool`.
|
||||
let func_node = ast::ExprName {
|
||||
id: "bool".into(),
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
};
|
||||
let node1 = ast::ExprCall {
|
||||
func: Box::new(node.into()),
|
||||
let value_node = ast::ExprCall {
|
||||
func: Box::new(func_node.into()),
|
||||
arguments: Arguments {
|
||||
args: vec![if_test.clone()],
|
||||
keywords: vec![],
|
||||
@@ -140,15 +167,28 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
},
|
||||
range: TextRange::default(),
|
||||
};
|
||||
let node2 = ast::StmtReturn {
|
||||
value: Some(Box::new(node1.into())),
|
||||
let return_node = ast::StmtReturn {
|
||||
value: Some(Box::new(value_node.into())),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
checker.generator().stmt(&node2.into()),
|
||||
range,
|
||||
)));
|
||||
};
|
||||
Some(checker.generator().stmt(&return_node.into()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
NeedlessBool {
|
||||
condition: SourceCodeSnippet::from_str(condition),
|
||||
replacement: replacement.clone().map(SourceCodeSnippet::new),
|
||||
},
|
||||
range,
|
||||
);
|
||||
if let Some(replacement) = replacement {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
replacement,
|
||||
range,
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ SIM103.py:3:5: SIM103 [*] Return the condition `a` directly
|
||||
6 | | return False
|
||||
| |____________________^ SIM103
|
||||
|
|
||||
= help: Replace with `return a`
|
||||
= help: Replace with `return bool(a)`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 1 | def f():
|
||||
@@ -63,7 +63,7 @@ SIM103.py:21:5: SIM103 [*] Return the condition `b` directly
|
||||
24 | | return False
|
||||
| |____________________^ SIM103
|
||||
|
|
||||
= help: Replace with `return b`
|
||||
= help: Replace with `return bool(b)`
|
||||
|
||||
ℹ Unsafe fix
|
||||
18 18 | # SIM103
|
||||
@@ -89,7 +89,7 @@ SIM103.py:32:9: SIM103 [*] Return the condition `b` directly
|
||||
35 | | return False
|
||||
| |________________________^ SIM103
|
||||
|
|
||||
= help: Replace with `return b`
|
||||
= help: Replace with `return bool(b)`
|
||||
|
||||
ℹ Unsafe fix
|
||||
29 29 | if a:
|
||||
@@ -104,7 +104,7 @@ SIM103.py:32:9: SIM103 [*] Return the condition `b` directly
|
||||
37 34 |
|
||||
38 35 | def f():
|
||||
|
||||
SIM103.py:57:5: SIM103 Return the condition `a` directly
|
||||
SIM103.py:57:5: SIM103 [*] Return the condition `a` directly
|
||||
|
|
||||
55 | def f():
|
||||
56 | # SIM103 (but not fixable)
|
||||
@@ -115,7 +115,20 @@ SIM103.py:57:5: SIM103 Return the condition `a` directly
|
||||
60 | | return True
|
||||
| |___________________^ SIM103
|
||||
|
|
||||
= help: Replace with `return a`
|
||||
= help: Replace with `return not a`
|
||||
|
||||
ℹ Unsafe fix
|
||||
54 54 |
|
||||
55 55 | def f():
|
||||
56 56 | # SIM103 (but not fixable)
|
||||
57 |- if a:
|
||||
58 |- return False
|
||||
59 |- else:
|
||||
60 |- return True
|
||||
57 |+ return not a
|
||||
61 58 |
|
||||
62 59 |
|
||||
63 60 | def f():
|
||||
|
||||
SIM103.py:83:5: SIM103 Return the condition `a` directly
|
||||
|
|
||||
@@ -128,6 +141,6 @@ SIM103.py:83:5: SIM103 Return the condition `a` directly
|
||||
86 | | return False
|
||||
| |____________________^ SIM103
|
||||
|
|
||||
= help: Replace with `return a`
|
||||
= help: Inline condition
|
||||
|
||||
|
||||
|
||||
@@ -10,158 +10,241 @@ SIM114.py:2:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
5 | | b
|
||||
| |_____^ SIM114
|
||||
6 |
|
||||
7 | if x == 1:
|
||||
7 | if a: # we preserve comments, too!
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:7:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
5 | b
|
||||
6 |
|
||||
7 | / if x == 1:
|
||||
8 | | for _ in range(20):
|
||||
9 | | print("hello")
|
||||
10 | | elif x == 2:
|
||||
11 | | for _ in range(20):
|
||||
12 | | print("hello")
|
||||
7 | / if a: # we preserve comments, too!
|
||||
8 | | b
|
||||
9 | | elif c: # but not on the second branch
|
||||
10 | | b
|
||||
| |_____^ SIM114
|
||||
11 |
|
||||
12 | if x == 1:
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:12:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
10 | b
|
||||
11 |
|
||||
12 | / if x == 1:
|
||||
13 | | for _ in range(20):
|
||||
14 | | print("hello")
|
||||
15 | | elif x == 2:
|
||||
16 | | for _ in range(20):
|
||||
17 | | print("hello")
|
||||
| |______________________^ SIM114
|
||||
13 |
|
||||
14 | if x == 1:
|
||||
18 |
|
||||
19 | if x == 1:
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:14:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
SIM114.py:19:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
12 | print("hello")
|
||||
13 |
|
||||
14 | / if x == 1:
|
||||
15 | | if True:
|
||||
16 | | for _ in range(20):
|
||||
17 | | print("hello")
|
||||
18 | | elif x == 2:
|
||||
19 | | if True:
|
||||
20 | | for _ in range(20):
|
||||
21 | | print("hello")
|
||||
| |__________________________^ SIM114
|
||||
22 |
|
||||
23 | if x == 1:
|
||||
|
|
||||
|
||||
SIM114.py:23:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
21 | print("hello")
|
||||
22 |
|
||||
23 | / if x == 1:
|
||||
17 | print("hello")
|
||||
18 |
|
||||
19 | / if x == 1:
|
||||
20 | | if True:
|
||||
21 | | for _ in range(20):
|
||||
22 | | print("hello")
|
||||
23 | | elif x == 2:
|
||||
24 | | if True:
|
||||
25 | | for _ in range(20):
|
||||
26 | | print("hello")
|
||||
27 | | elif False:
|
||||
28 | | for _ in range(20):
|
||||
29 | | print("hello")
|
||||
30 | | elif x == 2:
|
||||
31 | | if True:
|
||||
32 | | for _ in range(20):
|
||||
33 | | print("hello")
|
||||
34 | | elif False:
|
||||
35 | | for _ in range(20):
|
||||
36 | | print("hello")
|
||||
| |__________________________^ SIM114
|
||||
37 |
|
||||
38 | if (
|
||||
27 |
|
||||
28 | if x == 1:
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:24:5: SIM114 Combine `if` branches using logical `or` operator
|
||||
SIM114.py:28:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
23 | if x == 1:
|
||||
24 | if True:
|
||||
26 | print("hello")
|
||||
27 |
|
||||
28 | / if x == 1:
|
||||
29 | | if True:
|
||||
30 | | for _ in range(20):
|
||||
31 | | print("hello")
|
||||
32 | | elif False:
|
||||
33 | | for _ in range(20):
|
||||
34 | | print("hello")
|
||||
35 | | elif x == 2:
|
||||
36 | | if True:
|
||||
37 | | for _ in range(20):
|
||||
38 | | print("hello")
|
||||
39 | | elif False:
|
||||
40 | | for _ in range(20):
|
||||
41 | | print("hello")
|
||||
| |__________________________^ SIM114
|
||||
42 |
|
||||
43 | if (
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:29:5: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
28 | if x == 1:
|
||||
29 | if True:
|
||||
| _____^
|
||||
25 | | for _ in range(20):
|
||||
26 | | print("hello")
|
||||
27 | | elif False:
|
||||
28 | | for _ in range(20):
|
||||
29 | | print("hello")
|
||||
30 | | for _ in range(20):
|
||||
31 | | print("hello")
|
||||
32 | | elif False:
|
||||
33 | | for _ in range(20):
|
||||
34 | | print("hello")
|
||||
| |__________________________^ SIM114
|
||||
30 | elif x == 2:
|
||||
31 | if True:
|
||||
35 | elif x == 2:
|
||||
36 | if True:
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:31:5: SIM114 Combine `if` branches using logical `or` operator
|
||||
SIM114.py:36:5: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
29 | print("hello")
|
||||
30 | elif x == 2:
|
||||
31 | if True:
|
||||
34 | print("hello")
|
||||
35 | elif x == 2:
|
||||
36 | if True:
|
||||
| _____^
|
||||
32 | | for _ in range(20):
|
||||
33 | | print("hello")
|
||||
34 | | elif False:
|
||||
35 | | for _ in range(20):
|
||||
36 | | print("hello")
|
||||
37 | | for _ in range(20):
|
||||
38 | | print("hello")
|
||||
39 | | elif False:
|
||||
40 | | for _ in range(20):
|
||||
41 | | print("hello")
|
||||
| |__________________________^ SIM114
|
||||
37 |
|
||||
38 | if (
|
||||
42 |
|
||||
43 | if (
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:38:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
SIM114.py:43:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
36 | print("hello")
|
||||
37 |
|
||||
38 | / if (
|
||||
39 | | x == 1
|
||||
40 | | and y == 2
|
||||
41 | | and z == 3
|
||||
42 | | and a == 4
|
||||
43 | | and b == 5
|
||||
44 | | and c == 6
|
||||
45 | | and d == 7
|
||||
46 | | and e == 8
|
||||
47 | | and f == 9
|
||||
48 | | and g == 10
|
||||
49 | | and h == 11
|
||||
50 | | and i == 12
|
||||
51 | | and j == 13
|
||||
52 | | and k == 14
|
||||
53 | | ):
|
||||
54 | | pass
|
||||
55 | | elif 1 == 2:
|
||||
56 | | pass
|
||||
41 | print("hello")
|
||||
42 |
|
||||
43 | / if (
|
||||
44 | | x == 1
|
||||
45 | | and y == 2
|
||||
46 | | and z == 3
|
||||
47 | | and a == 4
|
||||
48 | | and b == 5
|
||||
49 | | and c == 6
|
||||
50 | | and d == 7
|
||||
51 | | and e == 8
|
||||
52 | | and f == 9
|
||||
53 | | and g == 10
|
||||
54 | | and h == 11
|
||||
55 | | and i == 12
|
||||
56 | | and j == 13
|
||||
57 | | and k == 14
|
||||
58 | | ):
|
||||
59 | | pass
|
||||
60 | | elif 1 == 2:
|
||||
61 | | pass
|
||||
| |________^ SIM114
|
||||
57 |
|
||||
58 | if result.eofs == "O":
|
||||
62 |
|
||||
63 | if result.eofs == "O":
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:62:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
SIM114.py:67:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
60 | elif result.eofs == "S":
|
||||
61 | skipped = 1
|
||||
62 | / elif result.eofs == "F":
|
||||
63 | | errors = 1
|
||||
64 | | elif result.eofs == "E":
|
||||
65 | | errors = 1
|
||||
65 | elif result.eofs == "S":
|
||||
66 | skipped = 1
|
||||
67 | / elif result.eofs == "F":
|
||||
68 | | errors = 1
|
||||
69 | | elif result.eofs == "E":
|
||||
70 | | errors = 1
|
||||
| |______________^ SIM114
|
||||
71 | elif result.eofs == "X":
|
||||
72 | errors = 1
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:69:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
67 | elif result.eofs == "F":
|
||||
68 | errors = 1
|
||||
69 | / elif result.eofs == "E":
|
||||
70 | | errors = 1
|
||||
71 | | elif result.eofs == "X":
|
||||
72 | | errors = 1
|
||||
| |______________^ SIM114
|
||||
73 | elif result.eofs == "C":
|
||||
74 | errors = 1
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:71:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
69 | elif result.eofs == "E":
|
||||
70 | errors = 1
|
||||
71 | / elif result.eofs == "X":
|
||||
72 | | errors = 1
|
||||
73 | | elif result.eofs == "C":
|
||||
74 | | errors = 1
|
||||
| |______________^ SIM114
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:109:5: SIM114 Combine `if` branches using logical `or` operator
|
||||
SIM114.py:118:5: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
107 | a = True
|
||||
108 | b = False
|
||||
109 | if a > b: # end-of-line
|
||||
116 | a = True
|
||||
117 | b = False
|
||||
118 | if a > b: # end-of-line
|
||||
| _____^
|
||||
110 | | return 3
|
||||
111 | | elif a == b:
|
||||
112 | | return 3
|
||||
119 | | return 3
|
||||
120 | | elif a == b:
|
||||
121 | | return 3
|
||||
| |________________^ SIM114
|
||||
113 | elif a < b: # end-of-line
|
||||
114 | return 4
|
||||
122 | elif a < b: # end-of-line
|
||||
123 | return 4
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:113:5: SIM114 Combine `if` branches using logical `or` operator
|
||||
SIM114.py:122:5: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
111 | elif a == b:
|
||||
112 | return 3
|
||||
113 | elif a < b: # end-of-line
|
||||
120 | elif a == b:
|
||||
121 | return 3
|
||||
122 | elif a < b: # end-of-line
|
||||
| _____^
|
||||
114 | | return 4
|
||||
115 | | elif b is None:
|
||||
116 | | return 4
|
||||
123 | | return 4
|
||||
124 | | elif b is None:
|
||||
125 | | return 4
|
||||
| |________________^ SIM114
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:132:5: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
130 | a = True
|
||||
131 | b = False
|
||||
132 | if a > b: # end-of-line
|
||||
| _____^
|
||||
133 | | return 3
|
||||
134 | | elif a := 1:
|
||||
135 | | return 3
|
||||
| |________________^ SIM114
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:138:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
138 | / if a: # we preserve comments, too!
|
||||
139 | | b
|
||||
140 | | elif c: # but not on the second branch
|
||||
141 | | b
|
||||
| |_____^ SIM114
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
SIM114.py:144:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
144 | / if a: b # here's a comment
|
||||
145 | | elif c: b
|
||||
| |_________^ SIM114
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM114.py:2:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
1 | # Errors
|
||||
2 | / if a:
|
||||
3 | | b
|
||||
4 | | elif c:
|
||||
5 | | b
|
||||
| |_____^ SIM114
|
||||
6 |
|
||||
7 | if a: # we preserve comments, too!
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |-if a:
|
||||
3 |- b
|
||||
4 |-elif c:
|
||||
2 |+if a or c:
|
||||
5 3 | b
|
||||
6 4 |
|
||||
7 5 | if a: # we preserve comments, too!
|
||||
|
||||
SIM114.py:7:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
5 | b
|
||||
6 |
|
||||
7 | / if a: # we preserve comments, too!
|
||||
8 | | b
|
||||
9 | | elif c: # but not on the second branch
|
||||
10 | | b
|
||||
| |_____^ SIM114
|
||||
11 |
|
||||
12 | if x == 1:
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | elif c:
|
||||
5 5 | b
|
||||
6 6 |
|
||||
7 |-if a: # we preserve comments, too!
|
||||
8 |- b
|
||||
9 |-elif c: # but not on the second branch
|
||||
7 |+if a or c: # we preserve comments, too!
|
||||
10 8 | b
|
||||
11 9 |
|
||||
12 10 | if x == 1:
|
||||
|
||||
SIM114.py:12:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
10 | b
|
||||
11 |
|
||||
12 | / if x == 1:
|
||||
13 | | for _ in range(20):
|
||||
14 | | print("hello")
|
||||
15 | | elif x == 2:
|
||||
16 | | for _ in range(20):
|
||||
17 | | print("hello")
|
||||
| |______________________^ SIM114
|
||||
18 |
|
||||
19 | if x == 1:
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
9 9 | elif c: # but not on the second branch
|
||||
10 10 | b
|
||||
11 11 |
|
||||
12 |-if x == 1:
|
||||
13 |- for _ in range(20):
|
||||
14 |- print("hello")
|
||||
15 |-elif x == 2:
|
||||
12 |+if x == 1 or x == 2:
|
||||
16 13 | for _ in range(20):
|
||||
17 14 | print("hello")
|
||||
18 15 |
|
||||
|
||||
SIM114.py:19:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
17 | print("hello")
|
||||
18 |
|
||||
19 | / if x == 1:
|
||||
20 | | if True:
|
||||
21 | | for _ in range(20):
|
||||
22 | | print("hello")
|
||||
23 | | elif x == 2:
|
||||
24 | | if True:
|
||||
25 | | for _ in range(20):
|
||||
26 | | print("hello")
|
||||
| |__________________________^ SIM114
|
||||
27 |
|
||||
28 | if x == 1:
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 | for _ in range(20):
|
||||
17 17 | print("hello")
|
||||
18 18 |
|
||||
19 |-if x == 1:
|
||||
20 |- if True:
|
||||
21 |- for _ in range(20):
|
||||
22 |- print("hello")
|
||||
23 |-elif x == 2:
|
||||
19 |+if x == 1 or x == 2:
|
||||
24 20 | if True:
|
||||
25 21 | for _ in range(20):
|
||||
26 22 | print("hello")
|
||||
|
||||
SIM114.py:28:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
26 | print("hello")
|
||||
27 |
|
||||
28 | / if x == 1:
|
||||
29 | | if True:
|
||||
30 | | for _ in range(20):
|
||||
31 | | print("hello")
|
||||
32 | | elif False:
|
||||
33 | | for _ in range(20):
|
||||
34 | | print("hello")
|
||||
35 | | elif x == 2:
|
||||
36 | | if True:
|
||||
37 | | for _ in range(20):
|
||||
38 | | print("hello")
|
||||
39 | | elif False:
|
||||
40 | | for _ in range(20):
|
||||
41 | | print("hello")
|
||||
| |__________________________^ SIM114
|
||||
42 |
|
||||
43 | if (
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
25 25 | for _ in range(20):
|
||||
26 26 | print("hello")
|
||||
27 27 |
|
||||
28 |-if x == 1:
|
||||
29 |- if True:
|
||||
30 |- for _ in range(20):
|
||||
31 |- print("hello")
|
||||
32 |- elif False:
|
||||
33 |- for _ in range(20):
|
||||
34 |- print("hello")
|
||||
35 |-elif x == 2:
|
||||
28 |+if x == 1 or x == 2:
|
||||
36 29 | if True:
|
||||
37 30 | for _ in range(20):
|
||||
38 31 | print("hello")
|
||||
|
||||
SIM114.py:29:5: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
28 | if x == 1:
|
||||
29 | if True:
|
||||
| _____^
|
||||
30 | | for _ in range(20):
|
||||
31 | | print("hello")
|
||||
32 | | elif False:
|
||||
33 | | for _ in range(20):
|
||||
34 | | print("hello")
|
||||
| |__________________________^ SIM114
|
||||
35 | elif x == 2:
|
||||
36 | if True:
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 | print("hello")
|
||||
27 27 |
|
||||
28 28 | if x == 1:
|
||||
29 |- if True:
|
||||
30 |- for _ in range(20):
|
||||
31 |- print("hello")
|
||||
32 |- elif False:
|
||||
29 |+ if True or False:
|
||||
33 30 | for _ in range(20):
|
||||
34 31 | print("hello")
|
||||
35 32 | elif x == 2:
|
||||
|
||||
SIM114.py:36:5: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
34 | print("hello")
|
||||
35 | elif x == 2:
|
||||
36 | if True:
|
||||
| _____^
|
||||
37 | | for _ in range(20):
|
||||
38 | | print("hello")
|
||||
39 | | elif False:
|
||||
40 | | for _ in range(20):
|
||||
41 | | print("hello")
|
||||
| |__________________________^ SIM114
|
||||
42 |
|
||||
43 | if (
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
33 33 | for _ in range(20):
|
||||
34 34 | print("hello")
|
||||
35 35 | elif x == 2:
|
||||
36 |- if True:
|
||||
37 |- for _ in range(20):
|
||||
38 |- print("hello")
|
||||
39 |- elif False:
|
||||
36 |+ if True or False:
|
||||
40 37 | for _ in range(20):
|
||||
41 38 | print("hello")
|
||||
42 39 |
|
||||
|
||||
SIM114.py:43:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
41 | print("hello")
|
||||
42 |
|
||||
43 | / if (
|
||||
44 | | x == 1
|
||||
45 | | and y == 2
|
||||
46 | | and z == 3
|
||||
47 | | and a == 4
|
||||
48 | | and b == 5
|
||||
49 | | and c == 6
|
||||
50 | | and d == 7
|
||||
51 | | and e == 8
|
||||
52 | | and f == 9
|
||||
53 | | and g == 10
|
||||
54 | | and h == 11
|
||||
55 | | and i == 12
|
||||
56 | | and j == 13
|
||||
57 | | and k == 14
|
||||
58 | | ):
|
||||
59 | | pass
|
||||
60 | | elif 1 == 2:
|
||||
61 | | pass
|
||||
| |________^ SIM114
|
||||
62 |
|
||||
63 | if result.eofs == "O":
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
55 55 | and i == 12
|
||||
56 56 | and j == 13
|
||||
57 57 | and k == 14
|
||||
58 |-):
|
||||
59 |- pass
|
||||
60 |-elif 1 == 2:
|
||||
58 |+) or 1 == 2:
|
||||
61 59 | pass
|
||||
62 60 |
|
||||
63 61 | if result.eofs == "O":
|
||||
|
||||
SIM114.py:67:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
65 | elif result.eofs == "S":
|
||||
66 | skipped = 1
|
||||
67 | / elif result.eofs == "F":
|
||||
68 | | errors = 1
|
||||
69 | | elif result.eofs == "E":
|
||||
70 | | errors = 1
|
||||
| |______________^ SIM114
|
||||
71 | elif result.eofs == "X":
|
||||
72 | errors = 1
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | pass
|
||||
65 65 | elif result.eofs == "S":
|
||||
66 66 | skipped = 1
|
||||
67 |-elif result.eofs == "F":
|
||||
68 |- errors = 1
|
||||
69 |-elif result.eofs == "E":
|
||||
67 |+elif result.eofs == "F" or result.eofs == "E":
|
||||
70 68 | errors = 1
|
||||
71 69 | elif result.eofs == "X":
|
||||
72 70 | errors = 1
|
||||
|
||||
SIM114.py:69:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
67 | elif result.eofs == "F":
|
||||
68 | errors = 1
|
||||
69 | / elif result.eofs == "E":
|
||||
70 | | errors = 1
|
||||
71 | | elif result.eofs == "X":
|
||||
72 | | errors = 1
|
||||
| |______________^ SIM114
|
||||
73 | elif result.eofs == "C":
|
||||
74 | errors = 1
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
66 66 | skipped = 1
|
||||
67 67 | elif result.eofs == "F":
|
||||
68 68 | errors = 1
|
||||
69 |-elif result.eofs == "E":
|
||||
70 |- errors = 1
|
||||
71 |-elif result.eofs == "X":
|
||||
69 |+elif result.eofs == "E" or result.eofs == "X":
|
||||
72 70 | errors = 1
|
||||
73 71 | elif result.eofs == "C":
|
||||
74 72 | errors = 1
|
||||
|
||||
SIM114.py:71:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
69 | elif result.eofs == "E":
|
||||
70 | errors = 1
|
||||
71 | / elif result.eofs == "X":
|
||||
72 | | errors = 1
|
||||
73 | | elif result.eofs == "C":
|
||||
74 | | errors = 1
|
||||
| |______________^ SIM114
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
68 68 | errors = 1
|
||||
69 69 | elif result.eofs == "E":
|
||||
70 70 | errors = 1
|
||||
71 |-elif result.eofs == "X":
|
||||
72 |- errors = 1
|
||||
73 |-elif result.eofs == "C":
|
||||
71 |+elif result.eofs == "X" or result.eofs == "C":
|
||||
74 72 | errors = 1
|
||||
75 73 |
|
||||
76 74 |
|
||||
|
||||
SIM114.py:118:5: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
116 | a = True
|
||||
117 | b = False
|
||||
118 | if a > b: # end-of-line
|
||||
| _____^
|
||||
119 | | return 3
|
||||
120 | | elif a == b:
|
||||
121 | | return 3
|
||||
| |________________^ SIM114
|
||||
122 | elif a < b: # end-of-line
|
||||
123 | return 4
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
115 115 | def func():
|
||||
116 116 | a = True
|
||||
117 117 | b = False
|
||||
118 |- if a > b: # end-of-line
|
||||
119 |- return 3
|
||||
120 |- elif a == b:
|
||||
118 |+ if a > b or a == b: # end-of-line
|
||||
121 119 | return 3
|
||||
122 120 | elif a < b: # end-of-line
|
||||
123 121 | return 4
|
||||
|
||||
SIM114.py:122:5: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
120 | elif a == b:
|
||||
121 | return 3
|
||||
122 | elif a < b: # end-of-line
|
||||
| _____^
|
||||
123 | | return 4
|
||||
124 | | elif b is None:
|
||||
125 | | return 4
|
||||
| |________________^ SIM114
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
119 119 | return 3
|
||||
120 120 | elif a == b:
|
||||
121 121 | return 3
|
||||
122 |- elif a < b: # end-of-line
|
||||
123 |- return 4
|
||||
124 |- elif b is None:
|
||||
122 |+ elif a < b or b is None: # end-of-line
|
||||
125 123 | return 4
|
||||
126 124 |
|
||||
127 125 |
|
||||
|
||||
SIM114.py:132:5: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
130 | a = True
|
||||
131 | b = False
|
||||
132 | if a > b: # end-of-line
|
||||
| _____^
|
||||
133 | | return 3
|
||||
134 | | elif a := 1:
|
||||
135 | | return 3
|
||||
| |________________^ SIM114
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
129 129 | """Ensure that the named expression is parenthesized when merged."""
|
||||
130 130 | a = True
|
||||
131 131 | b = False
|
||||
132 |- if a > b: # end-of-line
|
||||
133 |- return 3
|
||||
134 |- elif a := 1:
|
||||
132 |+ if a > b or (a := 1): # end-of-line
|
||||
135 133 | return 3
|
||||
136 134 |
|
||||
137 135 |
|
||||
|
||||
SIM114.py:138:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
138 | / if a: # we preserve comments, too!
|
||||
139 | | b
|
||||
140 | | elif c: # but not on the second branch
|
||||
141 | | b
|
||||
| |_____^ SIM114
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
135 135 | return 3
|
||||
136 136 |
|
||||
137 137 |
|
||||
138 |-if a: # we preserve comments, too!
|
||||
139 |- b
|
||||
140 |-elif c: # but not on the second branch
|
||||
138 |+if a or c: # we preserve comments, too!
|
||||
141 139 | b
|
||||
142 140 |
|
||||
143 141 |
|
||||
|
||||
SIM114.py:144:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
||||
|
|
||||
144 | / if a: b # here's a comment
|
||||
145 | | elif c: b
|
||||
| |_________^ SIM114
|
||||
|
|
||||
= help: Combine `if` branches
|
||||
|
||||
ℹ Safe fix
|
||||
141 141 | b
|
||||
142 142 |
|
||||
143 143 |
|
||||
144 |-if a: b # here's a comment
|
||||
145 |-elif c: b
|
||||
144 |+if a or c: b # here's a comment
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ use anyhow::Result;
|
||||
|
||||
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, Decorator, Expr};
|
||||
use ruff_python_codegen::{Generator, Stylist};
|
||||
use ruff_python_semantic::{
|
||||
analyze, Binding, BindingKind, NodeId, ResolvedReference, SemanticModel,
|
||||
analyze, Binding, BindingKind, NodeId, ResolvedReference, ScopeKind, SemanticModel,
|
||||
};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -104,6 +104,35 @@ fn runtime_required_decorators(
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if an annotation will be inspected at runtime by the `dataclasses` module.
|
||||
///
|
||||
/// Specifically, detects whether an annotation is to either `dataclasses.InitVar` or
|
||||
/// `typing.ClassVar` within a `@dataclass` class definition.
|
||||
///
|
||||
/// See: <https://docs.python.org/3/library/dataclasses.html#init-only-variables>
|
||||
pub(crate) fn is_dataclass_meta_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
||||
// Determine whether the assignment is in a `@dataclass` class definition.
|
||||
if let ScopeKind::Class(class_def) = semantic.current_scope().kind {
|
||||
if class_def.decorator_list.iter().any(|decorator| {
|
||||
semantic
|
||||
.resolve_call_path(map_callable(&decorator.expression))
|
||||
.is_some_and(|call_path| {
|
||||
matches!(call_path.as_slice(), ["dataclasses", "dataclass"])
|
||||
})
|
||||
}) {
|
||||
// Determine whether the annotation is `typing.ClassVar` or `dataclasses.InitVar`.
|
||||
return semantic
|
||||
.resolve_call_path(map_subscript(annotation))
|
||||
.is_some_and(|call_path| {
|
||||
matches!(call_path.as_slice(), ["dataclasses", "InitVar"])
|
||||
|| semantic.match_typing_call_path(&call_path, "ClassVar")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is registered as a `singledispatch` interface.
|
||||
///
|
||||
/// For example, `fun` below is a `singledispatch` interface:
|
||||
|
||||
@@ -39,6 +39,7 @@ mod tests {
|
||||
#[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("init_var.py"))]
|
||||
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("snapshot.py"))]
|
||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
|
||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote.py"))]
|
||||
@@ -75,7 +76,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]
|
||||
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))]
|
||||
fn strict(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("strict_{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
@@ -86,7 +89,7 @@ mod tests {
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
init_var.py:5:25: TCH003 [*] Move standard library import `dataclasses.FrozenInstanceError` into a type-checking block
|
||||
|
|
||||
3 | from __future__ import annotations
|
||||
4 |
|
||||
5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
|
||||
| ^^^^^^^^^^^^^^^^^^^ TCH003
|
||||
6 | from pathlib import Path
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 |
|
||||
3 3 | from __future__ import annotations
|
||||
4 4 |
|
||||
5 |-from dataclasses import FrozenInstanceError, InitVar, dataclass
|
||||
5 |+from dataclasses import InitVar, dataclass
|
||||
6 6 | from pathlib import Path
|
||||
7 |+from typing import TYPE_CHECKING
|
||||
8 |+
|
||||
9 |+if TYPE_CHECKING:
|
||||
10 |+ from dataclasses import FrozenInstanceError
|
||||
7 11 |
|
||||
8 12 |
|
||||
9 13 | @dataclass
|
||||
|
||||
init_var.py:6:21: TCH003 [*] Move standard library import `pathlib.Path` into a type-checking block
|
||||
|
|
||||
5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
|
||||
6 | from pathlib import Path
|
||||
| ^^^^ TCH003
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
3 3 | from __future__ import annotations
|
||||
4 4 |
|
||||
5 5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
|
||||
6 |-from pathlib import Path
|
||||
6 |+from typing import TYPE_CHECKING
|
||||
7 |+
|
||||
8 |+if TYPE_CHECKING:
|
||||
9 |+ from pathlib import Path
|
||||
7 10 |
|
||||
8 11 |
|
||||
9 12 | @dataclass
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
init_var.py:6:21: TCH003 [*] Move standard library import `pathlib.Path` into a type-checking block
|
||||
|
|
||||
5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
|
||||
6 | from pathlib import Path
|
||||
| ^^^^ TCH003
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
3 3 | from __future__ import annotations
|
||||
4 4 |
|
||||
5 5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
|
||||
6 |-from pathlib import Path
|
||||
6 |+from typing import TYPE_CHECKING
|
||||
7 |+
|
||||
8 |+if TYPE_CHECKING:
|
||||
9 |+ from pathlib import Path
|
||||
7 10 |
|
||||
8 11 |
|
||||
9 12 | @dataclass
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))]
|
||||
#[test_case(Rule::MultipleImportsOnOneLine, Path::new("E40.py"))]
|
||||
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))]
|
||||
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
|
||||
@@ -6,14 +6,14 @@ pub(crate) use compound_statements::*;
|
||||
pub(crate) use doc_line_too_long::*;
|
||||
pub(crate) use errors::*;
|
||||
pub use errors::{IOError, SyntaxError};
|
||||
pub(crate) use imports::*;
|
||||
|
||||
pub(crate) use invalid_escape_sequence::*;
|
||||
pub(crate) use lambda_assignment::*;
|
||||
pub(crate) use line_too_long::*;
|
||||
pub(crate) use literal_comparisons::*;
|
||||
pub(crate) use missing_newline_at_end_of_file::*;
|
||||
pub(crate) use mixed_spaces_and_tabs::*;
|
||||
pub(crate) use module_import_not_at_top_of_file::*;
|
||||
pub(crate) use multiple_imports_on_one_line::*;
|
||||
pub(crate) use not_tests::*;
|
||||
pub(crate) use tab_indentation::*;
|
||||
pub(crate) use trailing_whitespace::*;
|
||||
@@ -26,7 +26,6 @@ mod bare_except;
|
||||
mod compound_statements;
|
||||
mod doc_line_too_long;
|
||||
mod errors;
|
||||
mod imports;
|
||||
mod invalid_escape_sequence;
|
||||
mod lambda_assignment;
|
||||
mod line_too_long;
|
||||
@@ -34,6 +33,8 @@ mod literal_comparisons;
|
||||
pub(crate) mod logical_lines;
|
||||
mod missing_newline_at_end_of_file;
|
||||
mod mixed_spaces_and_tabs;
|
||||
mod module_import_not_at_top_of_file;
|
||||
mod multiple_imports_on_one_line;
|
||||
mod not_tests;
|
||||
mod tab_indentation;
|
||||
mod trailing_whitespace;
|
||||
|
||||
@@ -1,38 +1,10 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Alias, PySourceType, Stmt};
|
||||
use ruff_python_ast::{PySourceType, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Check for multiple imports on one line.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// According to [PEP 8], "imports should usually be on separate lines."
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import sys, os
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import os
|
||||
/// import sys
|
||||
/// ```
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#imports
|
||||
#[violation]
|
||||
pub struct MultipleImportsOnOneLine;
|
||||
|
||||
impl Violation for MultipleImportsOnOneLine {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Multiple imports on one line")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports that are not at the top of the file. For Jupyter notebooks, this
|
||||
/// checks for imports that are not at the top of the cell.
|
||||
@@ -82,15 +54,6 @@ impl Violation for ModuleImportNotAtTopOfFile {
|
||||
}
|
||||
}
|
||||
|
||||
/// E401
|
||||
pub(crate) fn multiple_imports_on_one_line(checker: &mut Checker, stmt: &Stmt, names: &[Alias]) {
|
||||
if names.len() > 1 {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(MultipleImportsOnOneLine, stmt.range()));
|
||||
}
|
||||
}
|
||||
|
||||
/// E402
|
||||
pub(crate) fn module_import_not_at_top_of_file(checker: &mut Checker, stmt: &Stmt) {
|
||||
if checker.semantic().seen_import_boundary() && checker.semantic().at_top_level() {
|
||||
@@ -0,0 +1,120 @@
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Alias, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_trivia::indentation_at_offset;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Check for multiple imports on one line.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// According to [PEP 8], "imports should usually be on separate lines."
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import sys, os
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import os
|
||||
/// import sys
|
||||
/// ```
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#imports
|
||||
#[violation]
|
||||
pub struct MultipleImportsOnOneLine;
|
||||
|
||||
impl Violation for MultipleImportsOnOneLine {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Multiple imports on one line")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!("Split imports"))
|
||||
}
|
||||
}
|
||||
|
||||
/// E401
|
||||
pub(crate) fn multiple_imports_on_one_line(checker: &mut Checker, stmt: &Stmt, names: &[Alias]) {
|
||||
if names.len() > 1 {
|
||||
let mut diagnostic = Diagnostic::new(MultipleImportsOnOneLine, stmt.range());
|
||||
if checker.settings.preview.is_enabled() {
|
||||
diagnostic.set_fix(split_imports(
|
||||
stmt,
|
||||
names,
|
||||
checker.locator(),
|
||||
checker.indexer(),
|
||||
checker.stylist(),
|
||||
));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a [`Fix`] to split the imports across multiple statements.
|
||||
fn split_imports(
|
||||
stmt: &Stmt,
|
||||
names: &[Alias],
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
stylist: &Stylist,
|
||||
) -> Fix {
|
||||
if indexer.in_multi_statement_line(stmt, locator) {
|
||||
// Ex) `x = 1; import os, sys` (convert to `x = 1; import os; import sys`)
|
||||
let replacement = names
|
||||
.iter()
|
||||
.map(|alias| {
|
||||
let Alias {
|
||||
range: _,
|
||||
name,
|
||||
asname,
|
||||
} = alias;
|
||||
|
||||
if let Some(asname) = asname {
|
||||
format!("import {name} as {asname}")
|
||||
} else {
|
||||
format!("import {name}")
|
||||
}
|
||||
})
|
||||
.join("; ");
|
||||
|
||||
Fix::safe_edit(Edit::range_replacement(replacement, stmt.range()))
|
||||
} else {
|
||||
// Ex) `import os, sys` (convert to `import os\nimport sys`)
|
||||
let indentation = indentation_at_offset(stmt.start(), locator).unwrap_or_default();
|
||||
|
||||
// Generate newline-delimited imports.
|
||||
let replacement = names
|
||||
.iter()
|
||||
.map(|alias| {
|
||||
let Alias {
|
||||
range: _,
|
||||
name,
|
||||
asname,
|
||||
} = alias;
|
||||
|
||||
if let Some(asname) = asname {
|
||||
format!("{indentation}import {name} as {asname}")
|
||||
} else {
|
||||
format!("{indentation}import {name}")
|
||||
}
|
||||
})
|
||||
.join(stylist.line_ending().as_str());
|
||||
|
||||
Fix::safe_edit(Edit::range_replacement(
|
||||
replacement,
|
||||
TextRange::new(locator.line_start(stmt.start()), stmt.end()),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,14 @@ pub(crate) fn preview_type_comparison(checker: &mut Checker, compare: &ast::Expr
|
||||
.filter(|(_, op)| matches!(op, CmpOp::Eq | CmpOp::NotEq))
|
||||
.map(|((left, right), _)| (left, right))
|
||||
{
|
||||
// If either expression is a type...
|
||||
if is_type(left, checker.semantic()) || is_type(right, checker.semantic()) {
|
||||
// And neither is a `dtype`...
|
||||
if is_dtype(left, checker.semantic()) || is_dtype(right, checker.semantic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Disallow the comparison.
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
TypeComparison {
|
||||
preview: PreviewMode::Enabled,
|
||||
@@ -295,3 +302,23 @@ fn is_type(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`Expr`] appears to be a reference to a NumPy dtype, since:
|
||||
/// > `dtype` are a bit of a strange beast, but definitely best thought of as instances, not
|
||||
/// > classes, and they are meant to be comparable not just to their own class, but also to the
|
||||
/// corresponding scalar types (e.g., `x.dtype == np.float32`) and strings (e.g.,
|
||||
/// `x.dtype == ['i1,i4']`; basically, __eq__ always tries to do `dtype(other)`).
|
||||
fn is_dtype(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
match expr {
|
||||
// Ex) `np.dtype(obj)`
|
||||
Expr::Call(ast::ExprCall { func, .. }) => semantic
|
||||
.resolve_call_path(func)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["numpy", "dtype"])),
|
||||
// Ex) `obj.dtype`
|
||||
Expr::Attribute(ast::ExprAttribute { attr, .. }) => {
|
||||
// Ex) `obj.dtype`
|
||||
attr.as_str() == "dtype"
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,87 @@ E40.py:2:1: E401 Multiple imports on one line
|
||||
1 | #: E401
|
||||
2 | import os, sys
|
||||
| ^^^^^^^^^^^^^^ E401
|
||||
3 | #: Okay
|
||||
4 | import os
|
||||
3 |
|
||||
4 | #: Okay
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
E40.py:65:1: E401 Multiple imports on one line
|
||||
|
|
||||
64 | #: E401
|
||||
65 | import re as regex, string # also with a comment!
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
66 | import re as regex, string; x = 1
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
E40.py:66:1: E401 Multiple imports on one line
|
||||
|
|
||||
64 | #: E401
|
||||
65 | import re as regex, string # also with a comment!
|
||||
66 | import re as regex, string; x = 1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
67 |
|
||||
68 | x = 1; import re as regex, string
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
E40.py:68:8: E401 Multiple imports on one line
|
||||
|
|
||||
66 | import re as regex, string; x = 1
|
||||
67 |
|
||||
68 | x = 1; import re as regex, string
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
E40.py:72:5: E401 Multiple imports on one line
|
||||
|
|
||||
71 | def blah():
|
||||
72 | import datetime as dt, copy
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
73 |
|
||||
74 | def nested_and_tested():
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
E40.py:75:9: E401 Multiple imports on one line
|
||||
|
|
||||
74 | def nested_and_tested():
|
||||
75 | import builtins, textwrap as tw
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
76 |
|
||||
77 | x = 1; import re as regex, string
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
E40.py:77:16: E401 Multiple imports on one line
|
||||
|
|
||||
75 | import builtins, textwrap as tw
|
||||
76 |
|
||||
77 | x = 1; import re as regex, string
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
78 | import re as regex, string; x = 1
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
E40.py:78:9: E401 Multiple imports on one line
|
||||
|
|
||||
77 | x = 1; import re as regex, string
|
||||
78 | import re as regex, string; x = 1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
79 |
|
||||
80 | if True: import re as regex, string
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
E40.py:80:14: E401 Multiple imports on one line
|
||||
|
|
||||
78 | import re as regex, string; x = 1
|
||||
79 |
|
||||
80 | if True: import re as regex, string
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
|
||||
|
||||
@@ -1,32 +1,60 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E40.py:55:1: E402 Module level import not at top of file
|
||||
E40.py:56:1: E402 Module level import not at top of file
|
||||
|
|
||||
53 | VERSION = '1.2.3'
|
||||
54 |
|
||||
55 | import foo
|
||||
54 | VERSION = '1.2.3'
|
||||
55 |
|
||||
56 | import foo
|
||||
| ^^^^^^^^^^ E402
|
||||
56 | #: E402
|
||||
57 | import foo
|
||||
57 | #: E402
|
||||
58 | import foo
|
||||
|
|
||||
|
||||
E40.py:57:1: E402 Module level import not at top of file
|
||||
E40.py:58:1: E402 Module level import not at top of file
|
||||
|
|
||||
55 | import foo
|
||||
56 | #: E402
|
||||
57 | import foo
|
||||
56 | import foo
|
||||
57 | #: E402
|
||||
58 | import foo
|
||||
| ^^^^^^^^^^ E402
|
||||
58 |
|
||||
59 | a = 1
|
||||
59 |
|
||||
60 | a = 1
|
||||
|
|
||||
|
||||
E40.py:61:1: E402 Module level import not at top of file
|
||||
E40.py:62:1: E402 Module level import not at top of file
|
||||
|
|
||||
59 | a = 1
|
||||
60 |
|
||||
61 | import bar
|
||||
60 | a = 1
|
||||
61 |
|
||||
62 | import bar
|
||||
| ^^^^^^^^^^ E402
|
||||
63 |
|
||||
64 | #: E401
|
||||
|
|
||||
|
||||
E40.py:65:1: E402 Module level import not at top of file
|
||||
|
|
||||
64 | #: E401
|
||||
65 | import re as regex, string # also with a comment!
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E402
|
||||
66 | import re as regex, string; x = 1
|
||||
|
|
||||
|
||||
E40.py:66:1: E402 Module level import not at top of file
|
||||
|
|
||||
64 | #: E401
|
||||
65 | import re as regex, string # also with a comment!
|
||||
66 | import re as regex, string; x = 1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E402
|
||||
67 |
|
||||
68 | x = 1; import re as regex, string
|
||||
|
|
||||
|
||||
E40.py:68:8: E402 Module level import not at top of file
|
||||
|
|
||||
66 | import re as regex, string; x = 1
|
||||
67 |
|
||||
68 | x = 1; import re as regex, string
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E402
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E40.py:2:1: E401 [*] Multiple imports on one line
|
||||
|
|
||||
1 | #: E401
|
||||
2 | import os, sys
|
||||
| ^^^^^^^^^^^^^^ E401
|
||||
3 |
|
||||
4 | #: Okay
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | #: E401
|
||||
2 |-import os, sys
|
||||
2 |+import os
|
||||
3 |+import sys
|
||||
3 4 |
|
||||
4 5 | #: Okay
|
||||
5 6 | import os
|
||||
|
||||
E40.py:65:1: E401 [*] Multiple imports on one line
|
||||
|
|
||||
64 | #: E401
|
||||
65 | import re as regex, string # also with a comment!
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
66 | import re as regex, string; x = 1
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
ℹ Safe fix
|
||||
62 62 | import bar
|
||||
63 63 |
|
||||
64 64 | #: E401
|
||||
65 |-import re as regex, string # also with a comment!
|
||||
65 |+import re as regex
|
||||
66 |+import string # also with a comment!
|
||||
66 67 | import re as regex, string; x = 1
|
||||
67 68 |
|
||||
68 69 | x = 1; import re as regex, string
|
||||
|
||||
E40.py:66:1: E401 [*] Multiple imports on one line
|
||||
|
|
||||
64 | #: E401
|
||||
65 | import re as regex, string # also with a comment!
|
||||
66 | import re as regex, string; x = 1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
67 |
|
||||
68 | x = 1; import re as regex, string
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
ℹ Safe fix
|
||||
63 63 |
|
||||
64 64 | #: E401
|
||||
65 65 | import re as regex, string # also with a comment!
|
||||
66 |-import re as regex, string; x = 1
|
||||
66 |+import re as regex; import string; x = 1
|
||||
67 67 |
|
||||
68 68 | x = 1; import re as regex, string
|
||||
69 69 |
|
||||
|
||||
E40.py:68:8: E401 [*] Multiple imports on one line
|
||||
|
|
||||
66 | import re as regex, string; x = 1
|
||||
67 |
|
||||
68 | x = 1; import re as regex, string
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
ℹ Safe fix
|
||||
65 65 | import re as regex, string # also with a comment!
|
||||
66 66 | import re as regex, string; x = 1
|
||||
67 67 |
|
||||
68 |-x = 1; import re as regex, string
|
||||
68 |+x = 1; import re as regex; import string
|
||||
69 69 |
|
||||
70 70 |
|
||||
71 71 | def blah():
|
||||
|
||||
E40.py:72:5: E401 [*] Multiple imports on one line
|
||||
|
|
||||
71 | def blah():
|
||||
72 | import datetime as dt, copy
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
73 |
|
||||
74 | def nested_and_tested():
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
ℹ Safe fix
|
||||
69 69 |
|
||||
70 70 |
|
||||
71 71 | def blah():
|
||||
72 |- import datetime as dt, copy
|
||||
72 |+ import datetime as dt
|
||||
73 |+ import copy
|
||||
73 74 |
|
||||
74 75 | def nested_and_tested():
|
||||
75 76 | import builtins, textwrap as tw
|
||||
|
||||
E40.py:75:9: E401 [*] Multiple imports on one line
|
||||
|
|
||||
74 | def nested_and_tested():
|
||||
75 | import builtins, textwrap as tw
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
76 |
|
||||
77 | x = 1; import re as regex, string
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
ℹ Safe fix
|
||||
72 72 | import datetime as dt, copy
|
||||
73 73 |
|
||||
74 74 | def nested_and_tested():
|
||||
75 |- import builtins, textwrap as tw
|
||||
75 |+ import builtins
|
||||
76 |+ import textwrap as tw
|
||||
76 77 |
|
||||
77 78 | x = 1; import re as regex, string
|
||||
78 79 | import re as regex, string; x = 1
|
||||
|
||||
E40.py:77:16: E401 [*] Multiple imports on one line
|
||||
|
|
||||
75 | import builtins, textwrap as tw
|
||||
76 |
|
||||
77 | x = 1; import re as regex, string
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
78 | import re as regex, string; x = 1
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
ℹ Safe fix
|
||||
74 74 | def nested_and_tested():
|
||||
75 75 | import builtins, textwrap as tw
|
||||
76 76 |
|
||||
77 |- x = 1; import re as regex, string
|
||||
77 |+ x = 1; import re as regex; import string
|
||||
78 78 | import re as regex, string; x = 1
|
||||
79 79 |
|
||||
80 80 | if True: import re as regex, string
|
||||
|
||||
E40.py:78:9: E401 [*] Multiple imports on one line
|
||||
|
|
||||
77 | x = 1; import re as regex, string
|
||||
78 | import re as regex, string; x = 1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
79 |
|
||||
80 | if True: import re as regex, string
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
ℹ Safe fix
|
||||
75 75 | import builtins, textwrap as tw
|
||||
76 76 |
|
||||
77 77 | x = 1; import re as regex, string
|
||||
78 |- import re as regex, string; x = 1
|
||||
78 |+ import re as regex; import string; x = 1
|
||||
79 79 |
|
||||
80 80 | if True: import re as regex, string
|
||||
|
||||
E40.py:80:14: E401 [*] Multiple imports on one line
|
||||
|
|
||||
78 | import re as regex, string; x = 1
|
||||
79 |
|
||||
80 | if True: import re as regex, string
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
||||
|
|
||||
= help: Split imports
|
||||
|
||||
ℹ Safe fix
|
||||
77 77 | x = 1; import re as regex, string
|
||||
78 78 | import re as regex, string; x = 1
|
||||
79 79 |
|
||||
80 |- if True: import re as regex, string
|
||||
80 |+ if True: import re as regex; import string
|
||||
|
||||
|
||||
@@ -129,4 +129,11 @@ E721.py:59:4: E721 Use `is` and `is not` for type comparisons, or `isinstance()`
|
||||
61 | #: Okay
|
||||
|
|
||||
|
||||
E721.py:140:1: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
||||
|
|
||||
139 | #: E721
|
||||
140 | dtype == float
|
||||
| ^^^^^^^^^^^^^^ E721
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::BlankLineAfterLastSection, Path::new("sections.py"))]
|
||||
#[test_case(Rule::NoBlankLineAfterSection, Path::new("sections.py"))]
|
||||
#[test_case(Rule::BlankLineAfterLastSection, Path::new("D413.py"))]
|
||||
#[test_case(Rule::BlankLineAfterSummary, Path::new("D.py"))]
|
||||
#[test_case(Rule::NoBlankLineBeforeSection, Path::new("sections.py"))]
|
||||
#[test_case(Rule::CapitalizeSectionName, Path::new("sections.py"))]
|
||||
|
||||
@@ -1632,9 +1632,13 @@ fn common_section(
|
||||
}
|
||||
|
||||
let line_end = checker.stylist().line_ending().as_str();
|
||||
let last_line = context.following_lines().last();
|
||||
if last_line.map_or(true, |line| !line.trim().is_empty()) {
|
||||
if let Some(next) = next {
|
||||
|
||||
if let Some(next) = next {
|
||||
if context
|
||||
.following_lines()
|
||||
.last()
|
||||
.map_or(true, |line| !line.trim().is_empty())
|
||||
{
|
||||
if checker.enabled(Rule::NoBlankLineAfterSection) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
NoBlankLineAfterSection {
|
||||
@@ -1649,7 +1653,16 @@ fn common_section(
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
// The first blank line is the line containing the closing triple quotes, so we need at
|
||||
// least two.
|
||||
let num_blank_lines = context
|
||||
.following_lines()
|
||||
.rev()
|
||||
.take_while(|line| line.trim().is_empty())
|
||||
.count();
|
||||
if num_blank_lines < 2 {
|
||||
if checker.enabled(Rule::BlankLineAfterLastSection) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLineAfterLastSection {
|
||||
@@ -1659,7 +1672,11 @@ fn common_section(
|
||||
);
|
||||
// Add a newline after the section.
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
format!("{}{}", line_end, docstring.indentation),
|
||||
format!(
|
||||
"{}{}",
|
||||
line_end.repeat(2 - num_blank_lines),
|
||||
docstring.indentation
|
||||
),
|
||||
context.end(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
|
||||
---
|
||||
D413.py:1:1: D413 [*] Missing blank line after last section ("Returns")
|
||||
|
|
||||
1 | / """Do something.
|
||||
2 | |
|
||||
3 | | Args:
|
||||
4 | | x: the value
|
||||
5 | | with a hanging indent
|
||||
6 | |
|
||||
7 | | Returns:
|
||||
8 | | the value
|
||||
9 | | """
|
||||
| |___^ D413
|
||||
|
|
||||
= help: Add blank line after "Returns"
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 |
|
||||
7 7 | Returns:
|
||||
8 8 | the value
|
||||
9 |+
|
||||
10 |+
|
||||
9 11 | """
|
||||
10 12 |
|
||||
11 13 |
|
||||
|
||||
D413.py:13:5: D413 [*] Missing blank line after last section ("Returns")
|
||||
|
|
||||
12 | def func():
|
||||
13 | """Do something.
|
||||
| _____^
|
||||
14 | |
|
||||
15 | | Args:
|
||||
16 | | x: the value
|
||||
17 | | with a hanging indent
|
||||
18 | |
|
||||
19 | | Returns:
|
||||
20 | | the value
|
||||
21 | | """
|
||||
| |_______^ D413
|
||||
|
|
||||
= help: Add blank line after "Returns"
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 |
|
||||
19 19 | Returns:
|
||||
20 20 | the value
|
||||
21 |+
|
||||
21 22 | """
|
||||
22 23 |
|
||||
23 24 |
|
||||
|
||||
D413.py:52:5: D413 [*] Missing blank line after last section ("Returns")
|
||||
|
|
||||
51 | def func():
|
||||
52 | """Do something.
|
||||
| _____^
|
||||
53 | |
|
||||
54 | | Args:
|
||||
55 | | x: the value
|
||||
56 | | with a hanging indent
|
||||
57 | |
|
||||
58 | | Returns:
|
||||
59 | | the value"""
|
||||
| |____________________^ D413
|
||||
|
|
||||
= help: Add blank line after "Returns"
|
||||
|
||||
ℹ Safe fix
|
||||
56 56 | with a hanging indent
|
||||
57 57 |
|
||||
58 58 | Returns:
|
||||
59 |- the value"""
|
||||
59 |+ the value
|
||||
60 |+
|
||||
61 |+ """
|
||||
|
||||
|
||||
@@ -19,9 +19,147 @@ sections.py:65:5: D413 [*] Missing blank line after last section ("Returns")
|
||||
66 66 |
|
||||
67 |- Returns"""
|
||||
67 |+ Returns
|
||||
68 |+ """
|
||||
68 69 |
|
||||
69 70 |
|
||||
70 71 | @expect(_D213)
|
||||
68 |+
|
||||
69 |+ """
|
||||
68 70 |
|
||||
69 71 |
|
||||
70 72 | @expect(_D213)
|
||||
|
||||
sections.py:120:5: D413 [*] Missing blank line after last section ("Returns")
|
||||
|
|
||||
118 | @expect("D413: Missing blank line after last section ('Returns')")
|
||||
119 | def no_blank_line_after_last_section(): # noqa: D416
|
||||
120 | """Toggle the gizmo.
|
||||
| _____^
|
||||
121 | |
|
||||
122 | | Returns
|
||||
123 | | -------
|
||||
124 | | A value of some sort.
|
||||
125 | | """
|
||||
| |_______^ D413
|
||||
|
|
||||
= help: Add blank line after "Returns"
|
||||
|
||||
ℹ Safe fix
|
||||
122 122 | Returns
|
||||
123 123 | -------
|
||||
124 124 | A value of some sort.
|
||||
125 |+
|
||||
125 126 | """
|
||||
126 127 |
|
||||
127 128 |
|
||||
|
||||
sections.py:170:5: D413 [*] Missing blank line after last section ("Returns")
|
||||
|
|
||||
168 | @expect("D414: Section has no content ('Returns')")
|
||||
169 | def section_underline_overindented_and_contentless(): # noqa: D416
|
||||
170 | """Toggle the gizmo.
|
||||
| _____^
|
||||
171 | |
|
||||
172 | | Returns
|
||||
173 | | -------
|
||||
174 | | """
|
||||
| |_______^ D413
|
||||
|
|
||||
= help: Add blank line after "Returns"
|
||||
|
||||
ℹ Safe fix
|
||||
171 171 |
|
||||
172 172 | Returns
|
||||
173 173 | -------
|
||||
174 |+
|
||||
174 175 | """
|
||||
175 176 |
|
||||
176 177 |
|
||||
|
||||
sections.py:519:5: D413 [*] Missing blank line after last section ("Parameters")
|
||||
|
|
||||
518 | def replace_equals_with_dash():
|
||||
519 | """Equal length equals should be replaced with dashes.
|
||||
| _____^
|
||||
520 | |
|
||||
521 | | Parameters
|
||||
522 | | ==========
|
||||
523 | | """
|
||||
| |_______^ D413
|
||||
|
|
||||
= help: Add blank line after "Parameters"
|
||||
|
||||
ℹ Safe fix
|
||||
520 520 |
|
||||
521 521 | Parameters
|
||||
522 522 | ==========
|
||||
523 |+
|
||||
523 524 | """
|
||||
524 525 |
|
||||
525 526 |
|
||||
|
||||
sections.py:527:5: D413 [*] Missing blank line after last section ("Parameters")
|
||||
|
|
||||
526 | def replace_equals_with_dash2():
|
||||
527 | """Here, the length of equals is not the same.
|
||||
| _____^
|
||||
528 | |
|
||||
529 | | Parameters
|
||||
530 | | ===========
|
||||
531 | | """
|
||||
| |_______^ D413
|
||||
|
|
||||
= help: Add blank line after "Parameters"
|
||||
|
||||
ℹ Safe fix
|
||||
528 528 |
|
||||
529 529 | Parameters
|
||||
530 530 | ===========
|
||||
531 |+
|
||||
531 532 | """
|
||||
532 533 |
|
||||
533 534 |
|
||||
|
||||
sections.py:548:5: D413 [*] Missing blank line after last section ("Args")
|
||||
|
|
||||
547 | def lowercase_sub_section_header():
|
||||
548 | """Below, `returns:` should _not_ be considered a section header.
|
||||
| _____^
|
||||
549 | |
|
||||
550 | | Args:
|
||||
551 | | Here's a note.
|
||||
552 | |
|
||||
553 | | returns:
|
||||
554 | | """
|
||||
| |_______^ D413
|
||||
|
|
||||
= help: Add blank line after "Args"
|
||||
|
||||
ℹ Safe fix
|
||||
551 551 | Here's a note.
|
||||
552 552 |
|
||||
553 553 | returns:
|
||||
554 |+
|
||||
554 555 | """
|
||||
555 556 |
|
||||
556 557 |
|
||||
|
||||
sections.py:558:5: D413 [*] Missing blank line after last section ("Returns")
|
||||
|
|
||||
557 | def titlecase_sub_section_header():
|
||||
558 | """Below, `Returns:` should be considered a section header.
|
||||
| _____^
|
||||
559 | |
|
||||
560 | | Args:
|
||||
561 | | Here's a note.
|
||||
562 | |
|
||||
563 | | Returns:
|
||||
564 | | """
|
||||
| |_______^ D413
|
||||
|
|
||||
= help: Add blank line after "Returns"
|
||||
|
||||
ℹ Safe fix
|
||||
561 561 | Here's a note.
|
||||
562 562 |
|
||||
563 563 | Returns:
|
||||
564 |+
|
||||
564 565 | """
|
||||
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@ mod tests {
|
||||
use rustc_hash::FxHashSet;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::pylint;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::settings::types::PythonVersion;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
#[test_case(Rule::AndOrTernary, Path::new("and_or_ternary.py"))]
|
||||
#[test_case(Rule::AssertOnStringLiteral, Path::new("assert_on_string_literal.py"))]
|
||||
@@ -91,6 +92,7 @@ mod tests {
|
||||
Path::new("named_expr_without_context.py")
|
||||
)]
|
||||
#[test_case(Rule::NonlocalWithoutBinding, Path::new("nonlocal_without_binding.py"))]
|
||||
#[test_case(Rule::NonSlotAssignment, Path::new("non_slot_assignment.py"))]
|
||||
#[test_case(Rule::PropertyWithParameters, Path::new("property_with_parameters.py"))]
|
||||
#[test_case(
|
||||
Rule::RedefinedArgumentFromLocal,
|
||||
@@ -167,7 +169,9 @@ mod tests {
|
||||
#[test_case(Rule::NoClassmethodDecorator, Path::new("no_method_decorator.py"))]
|
||||
#[test_case(Rule::UnnecessaryDunderCall, Path::new("unnecessary_dunder_call.py"))]
|
||||
#[test_case(Rule::NoStaticmethodDecorator, Path::new("no_method_decorator.py"))]
|
||||
#[test_case(Rule::PotentialIndexError, Path::new("potential_index_error.py"))]
|
||||
#[test_case(Rule::SuperWithoutBrackets, Path::new("super_without_brackets.py"))]
|
||||
#[test_case(Rule::TooManyNestedBlocks, Path::new("too_many_nested_blocks.py"))]
|
||||
#[test_case(
|
||||
Rule::UnnecessaryDictIndexLookup,
|
||||
Path::new("unnecessary_dict_index_lookup.py")
|
||||
@@ -190,6 +194,25 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::UselessElseOnLoop, Path::new("useless_else_on_loop.py"))]
|
||||
#[test_case(Rule::CollapsibleElseIf, Path::new("collapsible_else_if.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("pylint").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_isinstance_calls() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
use ruff_python_ast::{ElifElseClause, Stmt};
|
||||
use anyhow::Result;
|
||||
|
||||
use ast::whitespace::indentation;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, ElifElseClause, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::pyupgrade::fixes::adjust_indentation;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `else` blocks that consist of a single `if` statement.
|
||||
@@ -40,27 +47,85 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
pub struct CollapsibleElseIf;
|
||||
|
||||
impl Violation for CollapsibleElseIf {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Use `elif` instead of `else` then `if`, to reduce indentation")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Convert to `elif`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PLR5501
|
||||
pub(crate) fn collapsible_else_if(elif_else_clauses: &[ElifElseClause]) -> Option<Diagnostic> {
|
||||
let Some(ElifElseClause {
|
||||
body,
|
||||
test: None,
|
||||
range,
|
||||
}) = elif_else_clauses.last()
|
||||
pub(crate) fn collapsible_else_if(checker: &mut Checker, stmt: &Stmt) {
|
||||
let Stmt::If(ast::StmtIf {
|
||||
elif_else_clauses, ..
|
||||
}) = stmt
|
||||
else {
|
||||
return None;
|
||||
return;
|
||||
};
|
||||
if let [first @ Stmt::If(_)] = body.as_slice() {
|
||||
return Some(Diagnostic::new(
|
||||
CollapsibleElseIf,
|
||||
TextRange::new(range.start(), first.start()),
|
||||
));
|
||||
|
||||
let Some(
|
||||
else_clause @ ElifElseClause {
|
||||
body, test: None, ..
|
||||
},
|
||||
) = elif_else_clauses.last()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let [first @ Stmt::If(ast::StmtIf { .. })] = body.as_slice() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
CollapsibleElseIf,
|
||||
TextRange::new(else_clause.start(), first.start()),
|
||||
);
|
||||
|
||||
if checker.settings.preview.is_enabled() {
|
||||
diagnostic.try_set_fix(|| {
|
||||
convert_to_elif(first, else_clause, checker.locator(), checker.stylist())
|
||||
});
|
||||
}
|
||||
None
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Generate [`Fix`] to convert an `else` block to an `elif` block.
|
||||
fn convert_to_elif(
|
||||
first: &Stmt,
|
||||
else_clause: &ElifElseClause,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Fix> {
|
||||
let inner_if_line_start = locator.line_start(first.start());
|
||||
let inner_if_line_end = locator.line_end(first.end());
|
||||
|
||||
// Identify the indentation of the loop itself (e.g., the `while` or `for`).
|
||||
let Some(indentation) = indentation(locator, else_clause) else {
|
||||
return Err(anyhow::anyhow!("`else` is expected to be on its own line"));
|
||||
};
|
||||
|
||||
// Dedent the content from the end of the `else` to the end of the `if`.
|
||||
let indented = adjust_indentation(
|
||||
TextRange::new(inner_if_line_start, inner_if_line_end),
|
||||
indentation,
|
||||
locator,
|
||||
stylist,
|
||||
)?;
|
||||
|
||||
// Strip the indent from the first line of the `if` statement, and add `el` to the start.
|
||||
let Some(unindented) = indented.strip_prefix(indentation) else {
|
||||
return Err(anyhow::anyhow!("indented block to start with indentation"));
|
||||
};
|
||||
let indented = format!("{indentation}el{unindented}");
|
||||
|
||||
Ok(Fix::safe_edit(Edit::replacement(
|
||||
indented,
|
||||
locator.line_start(else_clause.start()),
|
||||
inner_if_line_end,
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -39,8 +39,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - [`namespace-packages`]: List of packages that are defined as namespace
|
||||
/// packages.
|
||||
/// - `namespace-packages`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8: Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions)
|
||||
@@ -48,7 +47,6 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// [PEP 8]: https://www.python.org/dev/peps/pep-0008/
|
||||
/// [PEP 420]: https://www.python.org/dev/peps/pep-0420/
|
||||
/// [`namespace-packages`]: https://beta.ruff.rs/docs/settings/#namespace-packages
|
||||
#[violation]
|
||||
pub struct ImportPrivateName {
|
||||
name: String,
|
||||
|
||||
@@ -21,6 +21,12 @@ use crate::checkers::ast::Checker;
|
||||
/// ```python
|
||||
/// 1 in {1, 2, 3}
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as the use of a `set` literal will
|
||||
/// error at runtime if the sequence contains unhashable elements (like lists
|
||||
/// or dictionaries).
|
||||
///
|
||||
/// ## References
|
||||
/// - [What’s New In Python 3.2](https://docs.python.org/3/whatsnew/3.2.html#optimizations)
|
||||
#[violation]
|
||||
|
||||
@@ -41,7 +41,9 @@ pub(crate) use no_method_decorator::*;
|
||||
pub(crate) use no_self_use::*;
|
||||
pub(crate) use non_ascii_module_import::*;
|
||||
pub(crate) use non_ascii_name::*;
|
||||
pub(crate) use non_slot_assignment::*;
|
||||
pub(crate) use nonlocal_without_binding::*;
|
||||
pub(crate) use potential_index_error::*;
|
||||
pub(crate) use property_with_parameters::*;
|
||||
pub(crate) use redefined_argument_from_local::*;
|
||||
pub(crate) use redefined_loop_name::*;
|
||||
@@ -59,6 +61,7 @@ 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_nested_blocks::*;
|
||||
pub(crate) use too_many_positional::*;
|
||||
pub(crate) use too_many_public_methods::*;
|
||||
pub(crate) use too_many_return_statements::*;
|
||||
@@ -123,7 +126,9 @@ mod no_method_decorator;
|
||||
mod no_self_use;
|
||||
mod non_ascii_module_import;
|
||||
mod non_ascii_name;
|
||||
mod non_slot_assignment;
|
||||
mod nonlocal_without_binding;
|
||||
mod potential_index_error;
|
||||
mod property_with_parameters;
|
||||
mod redefined_argument_from_local;
|
||||
mod redefined_loop_name;
|
||||
@@ -141,6 +146,7 @@ mod too_many_arguments;
|
||||
mod too_many_boolean_expressions;
|
||||
mod too_many_branches;
|
||||
mod too_many_locals;
|
||||
mod too_many_nested_blocks;
|
||||
mod too_many_positional;
|
||||
mod too_many_public_methods;
|
||||
mod too_many_return_statements;
|
||||
|
||||
242
crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs
Normal file
242
crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
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, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for assignments to attributes that are not defined in `__slots__`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// When using `__slots__`, only the specified attributes are allowed.
|
||||
/// Attempting to assign to an attribute that is not defined in `__slots__`
|
||||
/// will result in an `AttributeError` at runtime.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// This rule can't detect `__slots__` implementations in superclasses, and
|
||||
/// so limits its analysis to classes that inherit from (at most) `object`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Student:
|
||||
/// __slots__ = ("name",)
|
||||
///
|
||||
/// def __init__(self, name, surname):
|
||||
/// self.name = name
|
||||
/// self.surname = surname # [assigning-non-slot]
|
||||
/// self.setup()
|
||||
///
|
||||
/// def setup(self):
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Student:
|
||||
/// __slots__ = ("name", "surname")
|
||||
///
|
||||
/// def __init__(self, name, surname):
|
||||
/// self.name = name
|
||||
/// self.surname = surname
|
||||
/// self.setup()
|
||||
///
|
||||
/// def setup(self):
|
||||
/// pass
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct NonSlotAssignment {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for NonSlotAssignment {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let NonSlotAssignment { name } = self;
|
||||
format!("Attribute `{name}` is not defined in class's `__slots__`")
|
||||
}
|
||||
}
|
||||
|
||||
/// E0237
|
||||
pub(crate) fn non_slot_assignment(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
// If the class inherits from another class (aside from `object`), then it's possible that
|
||||
// the parent class defines the relevant `__slots__`.
|
||||
if !class_def.bases().iter().all(|base| {
|
||||
checker
|
||||
.semantic()
|
||||
.resolve_call_path(base)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "object"]))
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
for attribute in is_attributes_not_in_slots(&class_def.body) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
NonSlotAssignment {
|
||||
name: attribute.name.to_string(),
|
||||
},
|
||||
attribute.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AttributeAssignment<'a> {
|
||||
/// The name of the attribute that is assigned to.
|
||||
name: &'a str,
|
||||
/// The range of the attribute that is assigned to.
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl Ranged for AttributeAssignment<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a list of attributes that are assigned to but not included in `__slots__`.
|
||||
fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
|
||||
// First, collect all the attributes that are assigned to `__slots__`.
|
||||
let mut slots = FxHashSet::default();
|
||||
for statement in body {
|
||||
match statement {
|
||||
// Ex) `__slots__ = ("name",)`
|
||||
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
|
||||
let [Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if id == "__slots__" {
|
||||
slots.extend(slots_attributes(value));
|
||||
}
|
||||
}
|
||||
|
||||
// Ex) `__slots__: Tuple[str, ...] = ("name",)`
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
target,
|
||||
value: Some(value),
|
||||
..
|
||||
}) => {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if id == "__slots__" {
|
||||
slots.extend(slots_attributes(value));
|
||||
}
|
||||
}
|
||||
|
||||
// Ex) `__slots__ += ("name",)`
|
||||
Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if id == "__slots__" {
|
||||
slots.extend(slots_attributes(value));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if slots.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// Second, find any assignments that aren't included in `__slots__`.
|
||||
let mut assignments = vec![];
|
||||
for statement in body {
|
||||
let Stmt::FunctionDef(ast::StmtFunctionDef { name, body, .. }) = statement else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if name == "__init__" {
|
||||
for statement in body {
|
||||
match statement {
|
||||
// Ex) `self.name = name`
|
||||
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
|
||||
let [Expr::Attribute(attribute)] = targets.as_slice() else {
|
||||
continue;
|
||||
};
|
||||
let Expr::Name(ast::ExprName { id, .. }) = attribute.value.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
if id == "self" && !slots.contains(attribute.attr.as_str()) {
|
||||
assignments.push(AttributeAssignment {
|
||||
name: &attribute.attr,
|
||||
range: attribute.range(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ex) `self.name: str = name`
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
|
||||
let Expr::Attribute(attribute) = target.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let Expr::Name(ast::ExprName { id, .. }) = attribute.value.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
if id == "self" && !slots.contains(attribute.attr.as_str()) {
|
||||
assignments.push(AttributeAssignment {
|
||||
name: &attribute.attr,
|
||||
range: attribute.range(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ex) `self.name += name`
|
||||
Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => {
|
||||
let Expr::Attribute(attribute) = target.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let Expr::Name(ast::ExprName { id, .. }) = attribute.value.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
if id == "self" && !slots.contains(attribute.attr.as_str()) {
|
||||
assignments.push(AttributeAssignment {
|
||||
name: &attribute.attr,
|
||||
range: attribute.range(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assignments
|
||||
}
|
||||
|
||||
/// Return an iterator over the attributes enumerated in the given `__slots__` value.
|
||||
fn slots_attributes(expr: &Expr) -> impl Iterator<Item = &str> {
|
||||
// Ex) `__slots__ = ("name",)`
|
||||
let elts_iter = match expr {
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. })
|
||||
| Expr::List(ast::ExprList { elts, .. })
|
||||
| Expr::Set(ast::ExprSet { elts, .. }) => Some(elts.iter().filter_map(|elt| match elt {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value.to_str()),
|
||||
_ => None,
|
||||
})),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Ex) `__slots__ = {"name": ...}`
|
||||
let keys_iter = match expr {
|
||||
Expr::Dict(ast::ExprDict { keys, .. }) => Some(keys.iter().filter_map(|key| match key {
|
||||
Some(Expr::StringLiteral(ast::ExprStringLiteral { value, .. })) => Some(value.to_str()),
|
||||
_ => None,
|
||||
})),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
elts_iter
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(keys_iter.into_iter().flatten())
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for hard-coded sequence accesses that are known to be out of bounds.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Attempting to access a sequence with an out-of-bounds index will cause an
|
||||
/// `IndexError` to be raised at runtime. When the sequence and index are
|
||||
/// defined statically (e.g., subscripts on `list` and `tuple` literals, with
|
||||
/// integer indexes), such errors can be detected ahead of time.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// print([0, 1, 2][3])
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct PotentialIndexError;
|
||||
|
||||
impl Violation for PotentialIndexError {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Potential IndexError")
|
||||
}
|
||||
}
|
||||
|
||||
/// PLE0643
|
||||
pub(crate) fn potential_index_error(checker: &mut Checker, value: &Expr, slice: &Expr) {
|
||||
// Determine the length of the sequence.
|
||||
let length = match value {
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. }) => {
|
||||
match i64::try_from(elts.len()) {
|
||||
Ok(length) => length,
|
||||
Err(_) => return,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Determine the index value.
|
||||
let index = match slice {
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(number_value),
|
||||
..
|
||||
}) => number_value.as_i64(),
|
||||
Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: ast::UnaryOp::USub,
|
||||
operand,
|
||||
..
|
||||
}) => match operand.as_ref() {
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(number_value),
|
||||
..
|
||||
}) => number_value.as_i64().map(|number| -number),
|
||||
_ => return,
|
||||
},
|
||||
_ => return,
|
||||
};
|
||||
|
||||
// Emit a diagnostic if the index is out of bounds. If the index can't be represented as an
|
||||
// `i64`, but the length _can_, then the index is definitely out of bounds.
|
||||
if index.map_or(true, |index| index >= length || index < -length) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(PotentialIndexError, slice.range()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use ast::ExceptHandler;
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for functions or methods with too many nested blocks.
|
||||
///
|
||||
/// By default, this rule allows up to five nested blocks.
|
||||
/// This can be configured using the [`pylint.max-nested-blocks`] option.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Functions or methods with too many nested blocks are harder to understand
|
||||
/// and maintain.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `pylint.max-nested-blocks`
|
||||
#[violation]
|
||||
pub struct TooManyNestedBlocks {
|
||||
nested_blocks: usize,
|
||||
max_nested_blocks: usize,
|
||||
}
|
||||
|
||||
impl Violation for TooManyNestedBlocks {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let TooManyNestedBlocks {
|
||||
nested_blocks,
|
||||
max_nested_blocks,
|
||||
} = self;
|
||||
format!("Too many nested blocks ({nested_blocks} > {max_nested_blocks})")
|
||||
}
|
||||
}
|
||||
|
||||
/// PLR1702
|
||||
pub(crate) fn too_many_nested_blocks(checker: &mut Checker, stmt: &Stmt) {
|
||||
// Only enforce nesting within functions or methods.
|
||||
if !checker.semantic().current_scope().kind.is_function() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the statement isn't a leaf node, we don't want to emit a diagnostic, since the diagnostic
|
||||
// will be emitted on the leaves.
|
||||
if has_nested_block(stmt) {
|
||||
return;
|
||||
}
|
||||
|
||||
let max_nested_blocks = checker.settings.pylint.max_nested_blocks;
|
||||
|
||||
// Traverse up the hierarchy, identifying the root node and counting the number of nested
|
||||
// blocks between the root and this leaf.
|
||||
let (count, root_id) =
|
||||
checker
|
||||
.semantic()
|
||||
.current_statement_ids()
|
||||
.fold((0, None), |(count, ancestor_id), id| {
|
||||
let stmt = checker.semantic().statement(id);
|
||||
if is_nested_block(stmt) {
|
||||
(count + 1, Some(id))
|
||||
} else {
|
||||
(count, ancestor_id)
|
||||
}
|
||||
});
|
||||
|
||||
let Some(root_id) = root_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If the number of nested blocks is less than the maximum, we don't want to emit a diagnostic.
|
||||
if count <= max_nested_blocks {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
TooManyNestedBlocks {
|
||||
nested_blocks: count,
|
||||
max_nested_blocks,
|
||||
},
|
||||
checker.semantic().statement(root_id).range(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns `true` if the given statement is a nested block.
|
||||
fn is_nested_block(stmt: &Stmt) -> bool {
|
||||
matches!(
|
||||
stmt,
|
||||
Stmt::If(_) | Stmt::While(_) | Stmt::For(_) | Stmt::Try(_) | Stmt::With(_)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns `true` if the given statement is a leaf node.
|
||||
fn has_nested_block(stmt: &Stmt) -> bool {
|
||||
match stmt {
|
||||
Stmt::If(ast::StmtIf {
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
}) => {
|
||||
body.iter().any(is_nested_block)
|
||||
|| elif_else_clauses
|
||||
.iter()
|
||||
.any(|elif_else| elif_else.body.iter().any(is_nested_block))
|
||||
}
|
||||
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
|
||||
body.iter().any(is_nested_block) || orelse.iter().any(is_nested_block)
|
||||
}
|
||||
Stmt::For(ast::StmtFor { body, orelse, .. }) => {
|
||||
body.iter().any(is_nested_block) || orelse.iter().any(is_nested_block)
|
||||
}
|
||||
Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
..
|
||||
}) => {
|
||||
body.iter().any(is_nested_block)
|
||||
|| handlers.iter().any(|handler| match handler {
|
||||
ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
|
||||
body, ..
|
||||
}) => body.iter().any(is_nested_block),
|
||||
})
|
||||
|| orelse.iter().any(is_nested_block)
|
||||
|| finalbody.iter().any(is_nested_block)
|
||||
}
|
||||
Stmt::With(ast::StmtWith { body, .. }) => body.iter().any(is_nested_block),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
use ruff_python_ast::{self as ast, ExceptHandler, MatchCase, Stmt};
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ast::whitespace::indentation;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::identifier;
|
||||
use ruff_python_ast::{self as ast, ExceptHandler, MatchCase, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::pyupgrade::fixes::adjust_indentation;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `else` clauses on loops without a `break` statement.
|
||||
@@ -42,15 +48,50 @@ use crate::checkers::ast::Checker;
|
||||
pub struct UselessElseOnLoop;
|
||||
|
||||
impl Violation for UselessElseOnLoop {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"`else` clause on loop without a `break` statement; remove the `else` and de-indent all the \
|
||||
code inside it"
|
||||
"`else` clause on loop without a `break` statement; remove the `else` and dedent its contents"
|
||||
)
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Remove `else`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PLW0120
|
||||
pub(crate) fn useless_else_on_loop(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
body: &[Stmt],
|
||||
orelse: &[Stmt],
|
||||
) {
|
||||
if orelse.is_empty() || loop_exits_early(body) {
|
||||
return;
|
||||
}
|
||||
|
||||
let else_range = identifier::else_(stmt, checker.locator().contents()).expect("else clause");
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UselessElseOnLoop, else_range);
|
||||
|
||||
if checker.settings.preview.is_enabled() {
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_else(
|
||||
stmt,
|
||||
orelse,
|
||||
else_range,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Returns `true` if the given body contains a `break` statement.
|
||||
fn loop_exits_early(body: &[Stmt]) -> bool {
|
||||
body.iter().any(|stmt| match stmt {
|
||||
Stmt::If(ast::StmtIf {
|
||||
@@ -91,17 +132,50 @@ fn loop_exits_early(body: &[Stmt]) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// PLW0120
|
||||
pub(crate) fn useless_else_on_loop(
|
||||
checker: &mut Checker,
|
||||
/// Generate a [`Fix`] to remove the `else` clause from the given statement.
|
||||
fn remove_else(
|
||||
stmt: &Stmt,
|
||||
body: &[Stmt],
|
||||
orelse: &[Stmt],
|
||||
) {
|
||||
if !orelse.is_empty() && !loop_exits_early(body) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
UselessElseOnLoop,
|
||||
identifier::else_(stmt, checker.locator().contents()).unwrap(),
|
||||
));
|
||||
else_range: TextRange,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Fix> {
|
||||
let Some(start) = orelse.first() else {
|
||||
return Err(anyhow::anyhow!("Empty `else` clause"));
|
||||
};
|
||||
let Some(end) = orelse.last() else {
|
||||
return Err(anyhow::anyhow!("Empty `else` clause"));
|
||||
};
|
||||
|
||||
let start_indentation = indentation(locator, start);
|
||||
if start_indentation.is_none() {
|
||||
// Inline `else` block (e.g., `else: x = 1`).
|
||||
Ok(Fix::safe_edit(Edit::deletion(
|
||||
else_range.start(),
|
||||
start.start(),
|
||||
)))
|
||||
} else {
|
||||
// Identify the indentation of the loop itself (e.g., the `while` or `for`).
|
||||
let Some(desired_indentation) = indentation(locator, stmt) else {
|
||||
return Err(anyhow::anyhow!("Compound statement cannot be inlined"));
|
||||
};
|
||||
|
||||
// Dedent the content from the end of the `else` to the end of the loop.
|
||||
let indented = adjust_indentation(
|
||||
TextRange::new(
|
||||
locator.full_line_end(else_range.start()),
|
||||
locator.full_line_end(end.end()),
|
||||
),
|
||||
desired_indentation,
|
||||
locator,
|
||||
stylist,
|
||||
)?;
|
||||
|
||||
// Replace the content from the start of the `else` to the end of the loop.
|
||||
Ok(Fix::safe_edit(Edit::replacement(
|
||||
indented,
|
||||
locator.line_start(else_range.start()),
|
||||
locator.full_line_end(end.end()),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ pub struct Settings {
|
||||
pub max_statements: usize,
|
||||
pub max_public_methods: usize,
|
||||
pub max_locals: usize,
|
||||
pub max_nested_blocks: usize,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
@@ -75,6 +76,7 @@ impl Default for Settings {
|
||||
max_statements: 50,
|
||||
max_public_methods: 20,
|
||||
max_locals: 15,
|
||||
max_nested_blocks: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
non_slot_assignment.py:6:9: PLE0237 Attribute `surname` is not defined in class's `__slots__`
|
||||
|
|
||||
4 | def __init__(self, name, surname):
|
||||
5 | self.name = name
|
||||
6 | self.surname = surname # [assigning-non-slot]
|
||||
| ^^^^^^^^^^^^ PLE0237
|
||||
7 | self.setup()
|
||||
|
|
||||
|
||||
non_slot_assignment.py:18:9: PLE0237 Attribute `middle_name` is not defined in class's `__slots__`
|
||||
|
|
||||
16 | def __init__(self, name, middle_name):
|
||||
17 | self.name = name
|
||||
18 | self.middle_name = middle_name # [assigning-non-slot]
|
||||
| ^^^^^^^^^^^^^^^^ PLE0237
|
||||
19 | self.setup()
|
||||
|
|
||||
|
||||
non_slot_assignment.py:42:9: PLE0237 Attribute `middle_name` is not defined in class's `__slots__`
|
||||
|
|
||||
40 | def __init__(self, name, middle_name):
|
||||
41 | self.name = name
|
||||
42 | self.middle_name = middle_name # [assigning-non-slot]
|
||||
| ^^^^^^^^^^^^^^^^ PLE0237
|
||||
43 | self.setup()
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
potential_index_error.py:1:17: PLE0643 Potential IndexError
|
||||
|
|
||||
1 | print([1, 2, 3][3]) # PLE0643
|
||||
| ^ PLE0643
|
||||
2 | print([1, 2, 3][-4]) # PLE0643
|
||||
3 | print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643
|
||||
|
|
||||
|
||||
potential_index_error.py:2:17: PLE0643 Potential IndexError
|
||||
|
|
||||
1 | print([1, 2, 3][3]) # PLE0643
|
||||
2 | print([1, 2, 3][-4]) # PLE0643
|
||||
| ^^ PLE0643
|
||||
3 | print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643
|
||||
4 | print([1, 2, 3][-999999999999999999999999999999999999999999]) # PLE0643
|
||||
|
|
||||
|
||||
potential_index_error.py:3:17: PLE0643 Potential IndexError
|
||||
|
|
||||
1 | print([1, 2, 3][3]) # PLE0643
|
||||
2 | print([1, 2, 3][-4]) # PLE0643
|
||||
3 | print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE0643
|
||||
4 | print([1, 2, 3][-999999999999999999999999999999999999999999]) # PLE0643
|
||||
|
|
||||
|
||||
potential_index_error.py:4:17: PLE0643 Potential IndexError
|
||||
|
|
||||
2 | print([1, 2, 3][-4]) # PLE0643
|
||||
3 | print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643
|
||||
4 | print([1, 2, 3][-999999999999999999999999999999999999999999]) # PLE0643
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE0643
|
||||
5 |
|
||||
6 | print([1, 2, 3][2]) # OK
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
too_many_nested_blocks.py:2:5: PLR1702 Too many nested blocks (6 > 5)
|
||||
|
|
||||
1 | def correct_fruits(fruits) -> bool:
|
||||
2 | if len(fruits) > 1: # PLR1702
|
||||
| _____^
|
||||
3 | | if "apple" in fruits:
|
||||
4 | | if "orange" in fruits:
|
||||
5 | | count = fruits["orange"]
|
||||
6 | | if count % 2:
|
||||
7 | | if "kiwi" in fruits:
|
||||
8 | | if count == 2:
|
||||
9 | | return True
|
||||
| |_______________________________________^ PLR1702
|
||||
10 | return False
|
||||
|
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ collapsible_else_if.py:37:5: PLR5501 Use `elif` instead of `else` then `if`, to
|
||||
| |________^ PLR5501
|
||||
39 | pass
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
collapsible_else_if.py:45:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
@@ -23,17 +24,71 @@ collapsible_else_if.py:45:5: PLR5501 Use `elif` instead of `else` then `if`, to
|
||||
47 | pass
|
||||
48 | else:
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
collapsible_else_if.py:58:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
collapsible_else_if.py:55:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
56 | elif True:
|
||||
57 | print(2)
|
||||
58 | else:
|
||||
53 | if 1:
|
||||
54 | pass
|
||||
55 | else:
|
||||
| _____^
|
||||
59 | | if True:
|
||||
56 | | # inner comment
|
||||
57 | | if 2:
|
||||
| |________^ PLR5501
|
||||
60 | print(3)
|
||||
61 | else:
|
||||
58 | pass
|
||||
59 | else:
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
collapsible_else_if.py:69:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
67 | elif True:
|
||||
68 | print(2)
|
||||
69 | else:
|
||||
| _____^
|
||||
70 | | if True:
|
||||
| |________^ PLR5501
|
||||
71 | print(3)
|
||||
72 | else:
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
collapsible_else_if.py:79:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
77 | if 1:
|
||||
78 | pass
|
||||
79 | else:
|
||||
| _____^
|
||||
80 | | if 2: pass
|
||||
| |________^ PLR5501
|
||||
81 | else: pass
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
collapsible_else_if.py:87:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
85 | if 1:
|
||||
86 | pass
|
||||
87 | else:
|
||||
| _____^
|
||||
88 | | if 2: pass
|
||||
| |________^ PLR5501
|
||||
89 | else:
|
||||
90 | pass
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
collapsible_else_if.py:96:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
94 | if 1:
|
||||
95 | pass
|
||||
96 | else:
|
||||
| _____^
|
||||
97 | | if 2:
|
||||
| |________^ PLR5501
|
||||
98 | pass
|
||||
99 | else: pass
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
useless_else_on_loop.py:9:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
|
||||
useless_else_on_loop.py:9:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
7 | if i % 2:
|
||||
8 | return i
|
||||
@@ -10,8 +10,9 @@ useless_else_on_loop.py:9:5: PLW0120 `else` clause on loop without a `break` sta
|
||||
10 | print("math is broken")
|
||||
11 | return None
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
useless_else_on_loop.py:18:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
|
||||
useless_else_on_loop.py:18:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
16 | while True:
|
||||
17 | return 1
|
||||
@@ -20,8 +21,9 @@ useless_else_on_loop.py:18:5: PLW0120 `else` clause on loop without a `break` st
|
||||
19 | print("math is broken")
|
||||
20 | return None
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
useless_else_on_loop.py:30:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
|
||||
useless_else_on_loop.py:30:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
28 | break
|
||||
29 |
|
||||
@@ -29,8 +31,9 @@ useless_else_on_loop.py:30:1: PLW0120 `else` clause on loop without a `break` st
|
||||
| ^^^^ PLW0120
|
||||
31 | print("or else!")
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
useless_else_on_loop.py:37:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
|
||||
useless_else_on_loop.py:37:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
35 | while False:
|
||||
36 | break
|
||||
@@ -38,8 +41,9 @@ useless_else_on_loop.py:37:1: PLW0120 `else` clause on loop without a `break` st
|
||||
| ^^^^ PLW0120
|
||||
38 | print("or else!")
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
useless_else_on_loop.py:42:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
|
||||
useless_else_on_loop.py:42:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
40 | for j in range(10):
|
||||
41 | pass
|
||||
@@ -48,8 +52,9 @@ useless_else_on_loop.py:42:1: PLW0120 `else` clause on loop without a `break` st
|
||||
43 | print("fat chance")
|
||||
44 | for j in range(10):
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
useless_else_on_loop.py:88:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
|
||||
useless_else_on_loop.py:88:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
86 | else:
|
||||
87 | print("all right")
|
||||
@@ -58,8 +63,9 @@ useless_else_on_loop.py:88:5: PLW0120 `else` clause on loop without a `break` st
|
||||
89 | return True
|
||||
90 | return False
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
useless_else_on_loop.py:98:9: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
|
||||
useless_else_on_loop.py:98:9: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
96 | for _ in range(3):
|
||||
97 | pass
|
||||
@@ -68,5 +74,17 @@ useless_else_on_loop.py:98:9: PLW0120 `else` clause on loop without a `break` st
|
||||
99 | if 1 < 2: # pylint: disable=comparison-of-constants
|
||||
100 | break
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
useless_else_on_loop.py:144:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
142 | for j in range(10):
|
||||
143 | pass
|
||||
144 | else:
|
||||
| ^^^^ PLW0120
|
||||
145 | # [useless-else-on-loop]
|
||||
146 | print("fat chance")
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
collapsible_else_if.py:37:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
35 | if 1:
|
||||
36 | pass
|
||||
37 | else:
|
||||
| _____^
|
||||
38 | | if 2:
|
||||
| |________^ PLR5501
|
||||
39 | pass
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
34 34 | def not_ok0():
|
||||
35 35 | if 1:
|
||||
36 36 | pass
|
||||
37 |- else:
|
||||
38 |- if 2:
|
||||
39 |- pass
|
||||
37 |+ elif 2:
|
||||
38 |+ pass
|
||||
40 39 |
|
||||
41 40 |
|
||||
42 41 | def not_ok1():
|
||||
|
||||
collapsible_else_if.py:45:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
43 | if 1:
|
||||
44 | pass
|
||||
45 | else:
|
||||
| _____^
|
||||
46 | | if 2:
|
||||
| |________^ PLR5501
|
||||
47 | pass
|
||||
48 | else:
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
42 42 | def not_ok1():
|
||||
43 43 | if 1:
|
||||
44 44 | pass
|
||||
45 |+ elif 2:
|
||||
46 |+ pass
|
||||
45 47 | else:
|
||||
46 |- if 2:
|
||||
47 |- pass
|
||||
48 |- else:
|
||||
49 |- pass
|
||||
48 |+ pass
|
||||
50 49 |
|
||||
51 50 |
|
||||
52 51 | def not_ok1_with_comments():
|
||||
|
||||
collapsible_else_if.py:55:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
53 | if 1:
|
||||
54 | pass
|
||||
55 | else:
|
||||
| _____^
|
||||
56 | | # inner comment
|
||||
57 | | if 2:
|
||||
| |________^ PLR5501
|
||||
58 | pass
|
||||
59 | else:
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
52 52 | def not_ok1_with_comments():
|
||||
53 53 | if 1:
|
||||
54 54 | pass
|
||||
55 |+ elif 2:
|
||||
56 |+ pass
|
||||
55 57 | else:
|
||||
56 |- # inner comment
|
||||
57 |- if 2:
|
||||
58 |- pass
|
||||
59 |- else:
|
||||
60 |- pass # final pass comment
|
||||
58 |+ pass # final pass comment
|
||||
61 59 |
|
||||
62 60 |
|
||||
63 61 | # Regression test for https://github.com/apache/airflow/blob/f1e1cdcc3b2826e68ba133f350300b5065bbca33/airflow/models/dag.py#L1737
|
||||
|
||||
collapsible_else_if.py:69:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
67 | elif True:
|
||||
68 | print(2)
|
||||
69 | else:
|
||||
| _____^
|
||||
70 | | if True:
|
||||
| |________^ PLR5501
|
||||
71 | print(3)
|
||||
72 | else:
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
66 66 | print(1)
|
||||
67 67 | elif True:
|
||||
68 68 | print(2)
|
||||
69 |+ elif True:
|
||||
70 |+ print(3)
|
||||
69 71 | else:
|
||||
70 |- if True:
|
||||
71 |- print(3)
|
||||
72 |- else:
|
||||
73 |- print(4)
|
||||
72 |+ print(4)
|
||||
74 73 |
|
||||
75 74 |
|
||||
76 75 | def not_ok3():
|
||||
|
||||
collapsible_else_if.py:79:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
77 | if 1:
|
||||
78 | pass
|
||||
79 | else:
|
||||
| _____^
|
||||
80 | | if 2: pass
|
||||
| |________^ PLR5501
|
||||
81 | else: pass
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
76 76 | def not_ok3():
|
||||
77 77 | if 1:
|
||||
78 78 | pass
|
||||
79 |- else:
|
||||
80 |- if 2: pass
|
||||
81 |- else: pass
|
||||
79 |+ elif 2: pass
|
||||
80 |+ else: pass
|
||||
82 81 |
|
||||
83 82 |
|
||||
84 83 | def not_ok4():
|
||||
|
||||
collapsible_else_if.py:87:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
85 | if 1:
|
||||
86 | pass
|
||||
87 | else:
|
||||
| _____^
|
||||
88 | | if 2: pass
|
||||
| |________^ PLR5501
|
||||
89 | else:
|
||||
90 | pass
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
84 84 | def not_ok4():
|
||||
85 85 | if 1:
|
||||
86 86 | pass
|
||||
87 |+ elif 2: pass
|
||||
87 88 | else:
|
||||
88 |- if 2: pass
|
||||
89 |- else:
|
||||
90 |- pass
|
||||
89 |+ pass
|
||||
91 90 |
|
||||
92 91 |
|
||||
93 92 | def not_ok5():
|
||||
|
||||
collapsible_else_if.py:96:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
94 | if 1:
|
||||
95 | pass
|
||||
96 | else:
|
||||
| _____^
|
||||
97 | | if 2:
|
||||
| |________^ PLR5501
|
||||
98 | pass
|
||||
99 | else: pass
|
||||
|
|
||||
= help: Convert to `elif`
|
||||
|
||||
ℹ Safe fix
|
||||
93 93 | def not_ok5():
|
||||
94 94 | if 1:
|
||||
95 95 | pass
|
||||
96 |- else:
|
||||
97 |- if 2:
|
||||
98 |- pass
|
||||
99 |- else: pass
|
||||
96 |+ elif 2:
|
||||
97 |+ pass
|
||||
98 |+ else: pass
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
useless_else_on_loop.py:9:5: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
7 | if i % 2:
|
||||
8 | return i
|
||||
9 | else: # [useless-else-on-loop]
|
||||
| ^^^^ PLW0120
|
||||
10 | print("math is broken")
|
||||
11 | return None
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | for i in range(10):
|
||||
7 7 | if i % 2:
|
||||
8 8 | return i
|
||||
9 |- else: # [useless-else-on-loop]
|
||||
10 |- print("math is broken")
|
||||
9 |+ print("math is broken")
|
||||
11 10 | return None
|
||||
12 11 |
|
||||
13 12 |
|
||||
|
||||
useless_else_on_loop.py:18:5: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
16 | while True:
|
||||
17 | return 1
|
||||
18 | else: # [useless-else-on-loop]
|
||||
| ^^^^ PLW0120
|
||||
19 | print("math is broken")
|
||||
20 | return None
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
ℹ Safe fix
|
||||
15 15 | """else + return is not acceptable."""
|
||||
16 16 | while True:
|
||||
17 17 | return 1
|
||||
18 |- else: # [useless-else-on-loop]
|
||||
19 |- print("math is broken")
|
||||
18 |+ print("math is broken")
|
||||
20 19 | return None
|
||||
21 20 |
|
||||
22 21 |
|
||||
|
||||
useless_else_on_loop.py:30:1: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
28 | break
|
||||
29 |
|
||||
30 | else: # [useless-else-on-loop]
|
||||
| ^^^^ PLW0120
|
||||
31 | print("or else!")
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
ℹ Safe fix
|
||||
27 27 | for _ in range(10):
|
||||
28 28 | break
|
||||
29 29 |
|
||||
30 |-else: # [useless-else-on-loop]
|
||||
31 |- print("or else!")
|
||||
30 |+print("or else!")
|
||||
32 31 |
|
||||
33 32 |
|
||||
34 33 | while True:
|
||||
|
||||
useless_else_on_loop.py:37:1: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
35 | while False:
|
||||
36 | break
|
||||
37 | else: # [useless-else-on-loop]
|
||||
| ^^^^ PLW0120
|
||||
38 | print("or else!")
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
ℹ Safe fix
|
||||
34 34 | while True:
|
||||
35 35 | while False:
|
||||
36 36 | break
|
||||
37 |-else: # [useless-else-on-loop]
|
||||
38 |- print("or else!")
|
||||
37 |+print("or else!")
|
||||
39 38 |
|
||||
40 39 | for j in range(10):
|
||||
41 40 | pass
|
||||
|
||||
useless_else_on_loop.py:42:1: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
40 | for j in range(10):
|
||||
41 | pass
|
||||
42 | else: # [useless-else-on-loop]
|
||||
| ^^^^ PLW0120
|
||||
43 | print("fat chance")
|
||||
44 | for j in range(10):
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
ℹ Safe fix
|
||||
39 39 |
|
||||
40 40 | for j in range(10):
|
||||
41 41 | pass
|
||||
42 |-else: # [useless-else-on-loop]
|
||||
43 |- print("fat chance")
|
||||
44 |- for j in range(10):
|
||||
45 |- break
|
||||
42 |+print("fat chance")
|
||||
43 |+for j in range(10):
|
||||
44 |+ break
|
||||
46 45 |
|
||||
47 46 |
|
||||
48 47 | def test_return_for2():
|
||||
|
||||
useless_else_on_loop.py:88:5: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
86 | else:
|
||||
87 | print("all right")
|
||||
88 | else: # [useless-else-on-loop]
|
||||
| ^^^^ PLW0120
|
||||
89 | return True
|
||||
90 | return False
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
ℹ Safe fix
|
||||
85 85 | break
|
||||
86 86 | else:
|
||||
87 87 | print("all right")
|
||||
88 |- else: # [useless-else-on-loop]
|
||||
89 |- return True
|
||||
88 |+ return True
|
||||
90 89 | return False
|
||||
91 90 |
|
||||
92 91 |
|
||||
|
||||
useless_else_on_loop.py:98:9: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
96 | for _ in range(3):
|
||||
97 | pass
|
||||
98 | else:
|
||||
| ^^^^ PLW0120
|
||||
99 | if 1 < 2: # pylint: disable=comparison-of-constants
|
||||
100 | break
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
ℹ Safe fix
|
||||
95 95 | for _ in range(10):
|
||||
96 96 | for _ in range(3):
|
||||
97 97 | pass
|
||||
98 |- else:
|
||||
99 |- if 1 < 2: # pylint: disable=comparison-of-constants
|
||||
100 |- break
|
||||
98 |+ if 1 < 2: # pylint: disable=comparison-of-constants
|
||||
99 |+ break
|
||||
101 100 | else:
|
||||
102 101 | return True
|
||||
103 102 | return False
|
||||
|
||||
useless_else_on_loop.py:144:5: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
||||
|
|
||||
142 | for j in range(10):
|
||||
143 | pass
|
||||
144 | else:
|
||||
| ^^^^ PLW0120
|
||||
145 | # [useless-else-on-loop]
|
||||
146 | print("fat chance")
|
||||
|
|
||||
= help: Remove `else`
|
||||
|
||||
ℹ Safe fix
|
||||
141 141 | """Retain the comment within the `else` block"""
|
||||
142 142 | for j in range(10):
|
||||
143 143 | pass
|
||||
144 |- else:
|
||||
145 |- # [useless-else-on-loop]
|
||||
146 |- print("fat chance")
|
||||
147 |- for j in range(10):
|
||||
148 |- break
|
||||
144 |+ # [useless-else-on-loop]
|
||||
145 |+ print("fat chance")
|
||||
146 |+ for j in range(10):
|
||||
147 |+ break
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Rules from [pyupgrade](https://pypi.org/project/pyupgrade/).
|
||||
mod fixes;
|
||||
pub(crate) mod fixes;
|
||||
mod helpers;
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
|
||||
@@ -43,6 +43,10 @@ mod tests {
|
||||
#[test_case(Rule::NeverUnion, Path::new("RUF020.py"))]
|
||||
#[test_case(Rule::ParenthesizeChainedOperators, Path::new("RUF021.py"))]
|
||||
#[test_case(Rule::UnsortedDunderAll, Path::new("RUF022.py"))]
|
||||
#[test_case(Rule::UnsortedDunderSlots, Path::new("RUF023.py"))]
|
||||
#[test_case(Rule::MutableFromkeysValue, Path::new("RUF024.py"))]
|
||||
#[test_case(Rule::UnnecessaryDictComprehensionForIterable, Path::new("RUF025.py"))]
|
||||
#[test_case(Rule::DefaultFactoryKwarg, Path::new("RUF026.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
163
crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs
Normal file
163
crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use ast::Keyword;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_constant;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::{remove_argument, Parentheses};
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for incorrect usages of `default_factory` as a keyword argument when
|
||||
/// initializing a `defaultdict`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `defaultdict` constructor accepts a callable as its first argument.
|
||||
/// For example, it's common to initialize a `defaultdict` with `int` or `list`
|
||||
/// via `defaultdict(int)` or `defaultdict(list)`, to create a dictionary that
|
||||
/// returns `0` or `[]` respectively when a key is missing.
|
||||
///
|
||||
/// The default factory _must_ be provided as a positional argument, as all
|
||||
/// keyword arguments to `defaultdict` are interpreted as initial entries in
|
||||
/// the dictionary. For example, `defaultdict(foo=1, bar=2)` will create a
|
||||
/// dictionary with `{"foo": 1, "bar": 2}` as its initial entries.
|
||||
///
|
||||
/// As such, `defaultdict(default_factory=list)` will create a dictionary with
|
||||
/// `{"default_factory": list}` as its initial entry, instead of a dictionary
|
||||
/// that returns `[]` when a key is missing. Specifying a `default_factory`
|
||||
/// keyword argument is almost always a mistake, and one that type checkers
|
||||
/// can't reliably detect.
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as converting `default_factory` from a
|
||||
/// keyword to a positional argument will change the behavior of the code, even
|
||||
/// if the keyword argument was used erroneously.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// defaultdict(default_factory=int)
|
||||
/// defaultdict(default_factory=list)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// defaultdict(int)
|
||||
/// defaultdict(list)
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct DefaultFactoryKwarg {
|
||||
default_factory: SourceCodeSnippet,
|
||||
}
|
||||
|
||||
impl Violation for DefaultFactoryKwarg {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`default_factory` is a positional-only argument to `defaultdict`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let DefaultFactoryKwarg { default_factory } = self;
|
||||
if let Some(default_factory) = default_factory.full_display() {
|
||||
Some(format!("Replace with `defaultdict({default_factory})`"))
|
||||
} else {
|
||||
Some("Use positional argument".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF026
|
||||
pub(crate) fn default_factory_kwarg(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
// If the call isn't a `defaultdict` constructor, return.
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(call.func.as_ref())
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["collections", "defaultdict"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user provided a positional argument for `default_factory`, return.
|
||||
if !call.arguments.args.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user didn't provide a `default_factory` keyword argument, return.
|
||||
let Some(keyword) = call.arguments.find_keyword("default_factory") else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If the value is definitively not callable, return.
|
||||
if is_non_callable_value(&keyword.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DefaultFactoryKwarg {
|
||||
default_factory: SourceCodeSnippet::from_str(checker.locator().slice(keyword)),
|
||||
},
|
||||
call.range(),
|
||||
);
|
||||
diagnostic.try_set_fix(|| convert_to_positional(call, keyword, checker.locator()));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Returns `true` if a value is definitively not callable (e.g., `1` or `[]`).
|
||||
fn is_non_callable_value(value: &Expr) -> bool {
|
||||
is_constant(value)
|
||||
|| matches!(value, |Expr::List(_)| Expr::Dict(_)
|
||||
| Expr::Set(_)
|
||||
| Expr::Tuple(_)
|
||||
| Expr::Slice(_)
|
||||
| Expr::ListComp(_)
|
||||
| Expr::SetComp(_)
|
||||
| Expr::DictComp(_)
|
||||
| Expr::GeneratorExp(_)
|
||||
| Expr::FString(_))
|
||||
}
|
||||
|
||||
/// Generate an [`Expr`] to replace `defaultdict(default_factory=callable)` with
|
||||
/// `defaultdict(callable)`.
|
||||
///
|
||||
/// For example, given `defaultdict(default_factory=list)`, generate `defaultdict(list)`.
|
||||
fn convert_to_positional(
|
||||
call: &ast::ExprCall,
|
||||
default_factory: &Keyword,
|
||||
locator: &Locator,
|
||||
) -> Result<Fix> {
|
||||
if call.arguments.len() == 1 {
|
||||
// Ex) `defaultdict(default_factory=list)`
|
||||
Ok(Fix::unsafe_edit(Edit::range_replacement(
|
||||
locator.slice(&default_factory.value).to_string(),
|
||||
default_factory.range(),
|
||||
)))
|
||||
} else {
|
||||
// Ex) `defaultdict(member=1, default_factory=list)`
|
||||
|
||||
// First, remove the `default_factory` keyword argument.
|
||||
let removal_edit = remove_argument(
|
||||
default_factory,
|
||||
&call.arguments,
|
||||
Parentheses::Preserve,
|
||||
locator.contents(),
|
||||
)?;
|
||||
|
||||
// Second, insert the value as the first positional argument.
|
||||
let insertion_edit = Edit::insertion(
|
||||
format!("{}, ", locator.slice(&default_factory.value)),
|
||||
call.arguments
|
||||
.arguments_source_order()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("`default_factory` keyword argument not found"))?
|
||||
.start(),
|
||||
);
|
||||
|
||||
Ok(Fix::unsafe_edits(insertion_edit, [removal_edit]))
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ pub(super) fn has_default_copy_semantics(
|
||||
analyze::class::any_call_path(class_def, semantic, &|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["pydantic", "BaseModel" | "BaseSettings"]
|
||||
["pydantic", "BaseModel" | "BaseSettings" | "BaseConfig"]
|
||||
| ["pydantic_settings", "BaseSettings"]
|
||||
| ["msgspec", "Struct"]
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ pub(crate) use ambiguous_unicode_character::*;
|
||||
pub(crate) use assignment_in_assert::*;
|
||||
pub(crate) use asyncio_dangling_task::*;
|
||||
pub(crate) use collection_literal_concatenation::*;
|
||||
pub(crate) use default_factory_kwarg::*;
|
||||
pub(crate) use explicit_f_string_type_conversion::*;
|
||||
pub(crate) use function_call_in_dataclass_default::*;
|
||||
pub(crate) use implicit_optional::*;
|
||||
@@ -9,12 +10,15 @@ pub(crate) use invalid_index_type::*;
|
||||
pub(crate) use invalid_pyproject_toml::*;
|
||||
pub(crate) use mutable_class_default::*;
|
||||
pub(crate) use mutable_dataclass_default::*;
|
||||
pub(crate) use mutable_fromkeys_value::*;
|
||||
pub(crate) use never_union::*;
|
||||
pub(crate) use pairwise_over_zipped::*;
|
||||
pub(crate) use parenthesize_logical_operators::*;
|
||||
pub(crate) use quadratic_list_summation::*;
|
||||
pub(crate) use sort_dunder_all::*;
|
||||
pub(crate) use sort_dunder_slots::*;
|
||||
pub(crate) use static_key_dict_comprehension::*;
|
||||
pub(crate) use unnecessary_dict_comprehension_for_iterable::*;
|
||||
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
|
||||
pub(crate) use unnecessary_key_check::*;
|
||||
pub(crate) use unused_noqa::*;
|
||||
@@ -24,6 +28,7 @@ mod assignment_in_assert;
|
||||
mod asyncio_dangling_task;
|
||||
mod collection_literal_concatenation;
|
||||
mod confusables;
|
||||
mod default_factory_kwarg;
|
||||
mod explicit_f_string_type_conversion;
|
||||
mod function_call_in_dataclass_default;
|
||||
mod helpers;
|
||||
@@ -32,11 +37,15 @@ mod invalid_index_type;
|
||||
mod invalid_pyproject_toml;
|
||||
mod mutable_class_default;
|
||||
mod mutable_dataclass_default;
|
||||
mod mutable_fromkeys_value;
|
||||
mod never_union;
|
||||
mod pairwise_over_zipped;
|
||||
mod parenthesize_logical_operators;
|
||||
mod sequence_sorting;
|
||||
mod sort_dunder_all;
|
||||
mod sort_dunder_slots;
|
||||
mod static_key_dict_comprehension;
|
||||
mod unnecessary_dict_comprehension_for_iterable;
|
||||
mod unnecessary_iterable_allocation_for_first_element;
|
||||
mod unnecessary_key_check;
|
||||
mod unused_noqa;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::analyze::typing::is_mutable_expr;
|
||||
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for mutable objects passed as a value argument to `dict.fromkeys`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// All values in the dictionary created by the `dict.fromkeys` method
|
||||
/// refer to the same instance of the provided object. If that object is
|
||||
/// modified, all values are modified, which can lead to unexpected behavior.
|
||||
/// For example, if the empty list (`[]`) is provided as the default value,
|
||||
/// all values in the dictionary will use the same list; as such, appending to
|
||||
/// any one entry will append to all entries.
|
||||
///
|
||||
/// Instead, use a comprehension to generate a dictionary with distinct
|
||||
/// instances of the default value.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// cities = dict.fromkeys(["UK", "Poland"], [])
|
||||
/// cities["UK"].append("London")
|
||||
/// cities["Poland"].append("Poznan")
|
||||
/// print(cities) # {'UK': ['London', 'Poznan'], 'Poland': ['London', 'Poznan']}
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// cities = {country: [] for country in ["UK", "Poland"]}
|
||||
/// cities["UK"].append("London")
|
||||
/// cities["Poland"].append("Poznan")
|
||||
/// print(cities) # {'UK': ['London'], 'Poland': ['Poznan']}
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as the edit will change the behavior of
|
||||
/// the program by using a distinct object for every value in the dictionary,
|
||||
/// rather than a shared mutable instance. In some cases, programs may rely on
|
||||
/// the previous behavior.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `dict.fromkeys`](https://docs.python.org/3/library/stdtypes.html#dict.fromkeys)
|
||||
#[violation]
|
||||
pub struct MutableFromkeysValue;
|
||||
|
||||
impl Violation for MutableFromkeysValue {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Do not pass mutable objects as values to `dict.fromkeys`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with comprehension".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF024
|
||||
pub(crate) fn mutable_fromkeys_value(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = call.func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Check that the call is to `dict.fromkeys`.
|
||||
if attr != "fromkeys" {
|
||||
return;
|
||||
}
|
||||
let Some(name_expr) = value.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if name_expr.id != "dict" {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin("dict") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the value parameter is a mutable object.
|
||||
let [keys, value] = call.arguments.args.as_slice() else {
|
||||
return;
|
||||
};
|
||||
if !is_mutable_expr(value, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(MutableFromkeysValue, call.range());
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
generate_dict_comprehension(keys, value, checker.generator()),
|
||||
call.range(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Format a code snippet to expression `{key: value for key in keys}`, where
|
||||
/// `keys` and `value` are the parameters of `dict.fromkeys`.
|
||||
fn generate_dict_comprehension(keys: &Expr, value: &Expr, generator: Generator) -> String {
|
||||
// Construct `key`.
|
||||
let key = ast::ExprName {
|
||||
id: "key".to_string(),
|
||||
ctx: ast::ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
};
|
||||
// Construct `key in keys`.
|
||||
let comp = ast::Comprehension {
|
||||
target: key.clone().into(),
|
||||
iter: keys.clone(),
|
||||
ifs: vec![],
|
||||
range: TextRange::default(),
|
||||
is_async: false,
|
||||
};
|
||||
// Construct the dict comprehension.
|
||||
let dict_comp = ast::ExprDictComp {
|
||||
key: Box::new(key.into()),
|
||||
value: Box::new(value.clone()),
|
||||
generators: vec![comp],
|
||||
range: TextRange::default(),
|
||||
};
|
||||
generator.expr(&dict_comp.into())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user