Compare commits
40 Commits
v0.0.268
...
charlie/di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9fc63331a | ||
|
|
df3b95a73d | ||
|
|
a4432102f1 | ||
|
|
3295ccfbc4 | ||
|
|
7b315b84e2 | ||
|
|
bb2adb3017 | ||
|
|
5536d2befc | ||
|
|
b4824979b0 | ||
|
|
8a2f58065e | ||
|
|
8ca3977602 | ||
|
|
6db05d8cc6 | ||
|
|
fc63c6f2e2 | ||
|
|
f7f5bc9085 | ||
|
|
6b85430a14 | ||
|
|
a68c865010 | ||
|
|
fe7f2e2e4d | ||
|
|
0a3cf8ba11 | ||
|
|
bf5b463c0d | ||
|
|
6aa9900c03 | ||
|
|
9e21414294 | ||
|
|
bb4e674415 | ||
|
|
b42ff08612 | ||
|
|
03fb62c174 | ||
|
|
2dfc645ea9 | ||
|
|
fe8e2bb237 | ||
|
|
a9ed8d5391 | ||
|
|
41a681531d | ||
|
|
837e70677b | ||
|
|
7ebe372122 | ||
|
|
625849b846 | ||
|
|
32f1edc555 | ||
|
|
2f35099f81 | ||
|
|
ce8fd31a8f | ||
|
|
fdb241cad2 | ||
|
|
ab303f4e09 | ||
|
|
15cb21a6f4 | ||
|
|
2e2ba2cb16 | ||
|
|
d4c0a41b00 | ||
|
|
8702b5a40a | ||
|
|
bab818e801 |
14
.github/workflows/ci.yaml
vendored
14
.github/workflows/ci.yaml
vendored
@@ -183,18 +183,8 @@ jobs:
|
||||
- name: "Install cargo-udeps"
|
||||
uses: taiki-e/install-action@cargo-udeps
|
||||
- name: "Run cargo-udeps"
|
||||
run: |
|
||||
unused_dependencies=$(cargo +nightly-2023-03-30 udeps > unused.txt && cat unused.txt | cut -d $'\n' -f 2-)
|
||||
if [ -z "$unused_dependencies" ]; then
|
||||
echo "No unused dependencies found" > $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
else
|
||||
echo "Found unused dependencies" > $GITHUB_STEP_SUMMARY
|
||||
echo '```console' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$unused_dependencies" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
run: cargo +nightly-2023-03-30 udeps
|
||||
|
||||
|
||||
python-package:
|
||||
name: "python package"
|
||||
|
||||
@@ -134,7 +134,7 @@ Run `cargo dev generate-all` to generate the code for your new fixture. Then run
|
||||
locally with (e.g.) `cargo run -p ruff_cli -- check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
|
||||
|
||||
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
|
||||
`test_case` macro in the relevant `crates/ruff/src/[linter]/mod.rs` file. Then, run `cargo test`.
|
||||
`test_case` macro in the relevant `crates/ruff/src/rules/[linter]/mod.rs` file. Then, run `cargo test`.
|
||||
Your test will fail, but you'll be prompted to follow-up with `cargo insta review`. Accept the
|
||||
generated snapshot, then commit the snapshot file alongside the rest of your changes.
|
||||
|
||||
@@ -148,7 +148,7 @@ This implies that rule names:
|
||||
|
||||
- should state the bad thing being checked for
|
||||
|
||||
- should not contain instructions on what you what you should use instead
|
||||
- should not contain instructions on what you should use instead
|
||||
(these belong in the rule documentation and the `autofix_title` for rules that have autofix)
|
||||
|
||||
When re-implementing rules from other linters, this convention is given more importance than
|
||||
|
||||
33
Cargo.lock
generated
33
Cargo.lock
generated
@@ -193,9 +193,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.2.1"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
|
||||
checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
@@ -711,7 +711,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.267"
|
||||
version = "0.0.269"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.2.7",
|
||||
@@ -1723,11 +1723,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.267"
|
||||
version = "0.0.269"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
"bitflags 2.2.1",
|
||||
"bitflags 2.3.1",
|
||||
"chrono",
|
||||
"clap 4.2.7",
|
||||
"colored",
|
||||
@@ -1812,7 +1812,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.267"
|
||||
version = "0.0.269"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -1820,7 +1820,7 @@ dependencies = [
|
||||
"assert_cmd",
|
||||
"atty",
|
||||
"bincode",
|
||||
"bitflags 2.2.1",
|
||||
"bitflags 2.3.1",
|
||||
"cachedir",
|
||||
"chrono",
|
||||
"clap 4.2.7",
|
||||
@@ -1919,7 +1919,7 @@ name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.2.1",
|
||||
"bitflags 2.3.1",
|
||||
"is-macro",
|
||||
"itertools",
|
||||
"log",
|
||||
@@ -1927,7 +1927,6 @@ dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"rustpython-literal",
|
||||
@@ -1961,7 +1960,7 @@ dependencies = [
|
||||
name = "ruff_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.2.1",
|
||||
"bitflags 2.3.1",
|
||||
"is-macro",
|
||||
"nohash-hasher",
|
||||
"ruff_python_ast",
|
||||
@@ -2001,7 +2000,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruff_text_size"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -2072,7 +2071,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-ast"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
|
||||
dependencies = [
|
||||
"is-macro",
|
||||
"num-bigint",
|
||||
@@ -2083,9 +2082,9 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-format"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
|
||||
dependencies = [
|
||||
"bitflags 2.2.1",
|
||||
"bitflags 2.3.1",
|
||||
"itertools",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
@@ -2095,7 +2094,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-literal"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
|
||||
dependencies = [
|
||||
"hexf-parse",
|
||||
"is-macro",
|
||||
@@ -2107,7 +2106,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-parser"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"is-macro",
|
||||
@@ -2130,7 +2129,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-parser-core"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=e820928f11a2453314ad4d5ce23f066d1d3faf73#e820928f11a2453314ad4d5ce23f066d1d3faf73"
|
||||
source = "git+https://github.com/RustPython/Parser.git?rev=3654cf0bdfc270df6b2b83e2df086843574ad082#3654cf0bdfc270df6b2b83e2df086843574ad082"
|
||||
dependencies = [
|
||||
"is-macro",
|
||||
"ruff_text_size",
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -11,7 +11,7 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.69" }
|
||||
bitflags = { version = "2.2.1" }
|
||||
bitflags = { version = "2.3.1" }
|
||||
chrono = { version = "0.4.23", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.1.8", features = ["derive"] }
|
||||
colored = { version = "2.0.0" }
|
||||
@@ -31,10 +31,10 @@ proc-macro2 = { version = "1.0.51" }
|
||||
quote = { version = "1.0.23" }
|
||||
regex = { version = "1.7.1" }
|
||||
rustc-hash = { version = "1.1.0" }
|
||||
ruff_text_size = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73" }
|
||||
rustpython-format = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73" }
|
||||
rustpython-literal = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73" }
|
||||
rustpython-parser = { git = "https://github.com/RustPython/Parser.git", rev = "e820928f11a2453314ad4d5ce23f066d1d3faf73", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
|
||||
ruff_text_size = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082" }
|
||||
rustpython-format = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082" }
|
||||
rustpython-literal = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082" }
|
||||
rustpython-parser = { git = "https://github.com/RustPython/Parser.git", rev = "3654cf0bdfc270df6b2b83e2df086843574ad082", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
|
||||
schemars = { version = "0.8.12" }
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = { version = "1.0.93", features = ["preserve_order"] }
|
||||
|
||||
@@ -137,7 +137,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
|
||||
```yaml
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.0.267'
|
||||
rev: 'v0.0.269'
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -388,7 +388,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [SciPy](https://github.com/scipy/scipy)
|
||||
- [Sphinx](https://github.com/sphinx-doc/sphinx)
|
||||
- [Stable Baselines3](https://github.com/DLR-RM/stable-baselines3)
|
||||
- [Starlite](https://github.com/starlite-api/starlite)
|
||||
- [Litestar](https://litestar.dev/)
|
||||
- [The Algorithms](https://github.com/TheAlgorithms/Python)
|
||||
- [Vega-Altair](https://github.com/altair-viz/altair)
|
||||
- WordPress ([Openverse](https://github.com/WordPress/openverse))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.267"
|
||||
version = "0.0.269"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.267"
|
||||
version = "0.0.269"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
3
crates/ruff/resources/test/fixtures/flake8_bandit/S601.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/flake8_bandit/S601.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import paramiko
|
||||
|
||||
paramiko.exec_command('something; really; unsafe')
|
||||
65
crates/ruff/resources/test/fixtures/flake8_pyi/PYI013.py
vendored
Normal file
65
crates/ruff/resources/test/fixtures/flake8_pyi/PYI013.py
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
class OneAttributeClass:
|
||||
value: int
|
||||
...
|
||||
|
||||
|
||||
class OneAttributeClass2:
|
||||
...
|
||||
value: int
|
||||
|
||||
|
||||
class TwoEllipsesClass:
|
||||
...
|
||||
...
|
||||
|
||||
|
||||
class DocstringClass:
|
||||
"""
|
||||
My body only contains an ellipsis.
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class NonEmptyChild(Exception):
|
||||
value: int
|
||||
...
|
||||
|
||||
|
||||
class NonEmptyChild2(Exception):
|
||||
...
|
||||
value: int
|
||||
|
||||
|
||||
class NonEmptyWithInit:
|
||||
value: int
|
||||
...
|
||||
|
||||
def __init__():
|
||||
pass
|
||||
|
||||
|
||||
class EmptyClass:
|
||||
...
|
||||
|
||||
|
||||
class EmptyEllipsis:
|
||||
...
|
||||
|
||||
|
||||
class Dog:
|
||||
eyes: int = 2
|
||||
|
||||
|
||||
class WithInit:
|
||||
value: int = 0
|
||||
|
||||
def __init__():
|
||||
...
|
||||
|
||||
|
||||
def function():
|
||||
...
|
||||
|
||||
|
||||
...
|
||||
56
crates/ruff/resources/test/fixtures/flake8_pyi/PYI013.pyi
vendored
Normal file
56
crates/ruff/resources/test/fixtures/flake8_pyi/PYI013.pyi
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# Violations of PYI013
|
||||
|
||||
class OneAttributeClass:
|
||||
value: int
|
||||
... # Error
|
||||
|
||||
class OneAttributeClass2:
|
||||
... # Error
|
||||
value: int
|
||||
|
||||
class MyClass:
|
||||
...
|
||||
value: int
|
||||
|
||||
class TwoEllipsesClass:
|
||||
...
|
||||
... # Error
|
||||
|
||||
class DocstringClass:
|
||||
"""
|
||||
My body only contains an ellipsis.
|
||||
"""
|
||||
|
||||
... # Error
|
||||
|
||||
class NonEmptyChild(Exception):
|
||||
value: int
|
||||
... # Error
|
||||
|
||||
class NonEmptyChild2(Exception):
|
||||
... # Error
|
||||
value: int
|
||||
|
||||
class NonEmptyWithInit:
|
||||
value: int
|
||||
... # Error
|
||||
|
||||
def __init__():
|
||||
pass
|
||||
|
||||
# Not violations
|
||||
|
||||
class EmptyClass: ...
|
||||
class EmptyEllipsis: ...
|
||||
|
||||
class Dog:
|
||||
eyes: int = 2
|
||||
|
||||
class WithInit:
|
||||
value: int = 0
|
||||
|
||||
def __init__(): ...
|
||||
|
||||
def function(): ...
|
||||
|
||||
...
|
||||
@@ -14,7 +14,7 @@ if key not in a_dict:
|
||||
else:
|
||||
var = a_dict[key]
|
||||
|
||||
# SIM401 (default with a complex expression)
|
||||
# OK (default contains effect)
|
||||
if key in a_dict:
|
||||
var = a_dict[key]
|
||||
else:
|
||||
|
||||
@@ -19,7 +19,7 @@ if x > 0:
|
||||
else:
|
||||
import e
|
||||
|
||||
y = x + 1
|
||||
__some__magic = 1
|
||||
|
||||
import f
|
||||
|
||||
|
||||
10
crates/ruff/resources/test/fixtures/pyflakes/F401_12.py
vendored
Normal file
10
crates/ruff/resources/test/fixtures/pyflakes/F401_12.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Test: module bindings are preferred over local bindings, for deferred annotations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Class:
|
||||
datetime: Optional[datetime.datetime]
|
||||
12
crates/ruff/resources/test/fixtures/pyflakes/F401_13.py
vendored
Normal file
12
crates/ruff/resources/test/fixtures/pyflakes/F401_13.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Test: module bindings are preferred over local bindings, for deferred annotations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeAlias, List
|
||||
|
||||
|
||||
class Class:
|
||||
List: TypeAlias = List
|
||||
|
||||
def bar(self) -> List:
|
||||
pass
|
||||
8
crates/ruff/resources/test/fixtures/pyflakes/F401_14.py
vendored
Normal file
8
crates/ruff/resources/test/fixtures/pyflakes/F401_14.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Test: module bindings are preferred over local bindings, for deferred annotations."""
|
||||
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Class:
|
||||
datetime: "Optional[datetime.datetime]"
|
||||
11
crates/ruff/resources/test/fixtures/pylint/duplicate_value.py
vendored
Normal file
11
crates/ruff/resources/test/fixtures/pylint/duplicate_value.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
###
|
||||
# Errors.
|
||||
###
|
||||
incorrect_set = {"value1", 23, 5, "value1"}
|
||||
incorrect_set = {1, 1}
|
||||
|
||||
###
|
||||
# Non-errors.
|
||||
###
|
||||
correct_set = {"value1", 23, 5}
|
||||
correct_set = {5, "5"}
|
||||
19
crates/ruff/resources/test/fixtures/pylint/named_expr_without_context.py
vendored
Normal file
19
crates/ruff/resources/test/fixtures/pylint/named_expr_without_context.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Errors
|
||||
(a := 42)
|
||||
if True:
|
||||
(b := 1)
|
||||
|
||||
|
||||
class Foo:
|
||||
(c := 1)
|
||||
|
||||
|
||||
# OK
|
||||
if a := 42:
|
||||
print("Success")
|
||||
|
||||
a = 0
|
||||
while (a := a + 1) < 10:
|
||||
print("Correct")
|
||||
|
||||
a = (b := 1)
|
||||
28
crates/ruff/resources/test/fixtures/pyupgrade/UP032_2.py
vendored
Normal file
28
crates/ruff/resources/test/fixtures/pyupgrade/UP032_2.py
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Errors
|
||||
"{.real}".format(1)
|
||||
"{0.real}".format(1)
|
||||
"{a.real}".format(a=1)
|
||||
|
||||
"{.real}".format(1.0)
|
||||
"{0.real}".format(1.0)
|
||||
"{a.real}".format(a=1.0)
|
||||
|
||||
"{.real}".format(1j)
|
||||
"{0.real}".format(1j)
|
||||
"{a.real}".format(a=1j)
|
||||
|
||||
"{.real}".format(0b01)
|
||||
"{0.real}".format(0b01)
|
||||
"{a.real}".format(a=0b01)
|
||||
|
||||
"{}".format(1 + 2)
|
||||
"{}".format([1, 2])
|
||||
"{}".format({1, 2})
|
||||
"{}".format({1: 2, 3: 4})
|
||||
"{}".format((i for i in range(2)))
|
||||
|
||||
"{.real}".format(1 + 2)
|
||||
"{.real}".format([1, 2])
|
||||
"{.real}".format({1, 2})
|
||||
"{.real}".format({1: 2, 3: 4})
|
||||
"{}".format((i for i in range(2)))
|
||||
@@ -10,6 +10,8 @@ f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010
|
||||
|
||||
f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
|
||||
f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
|
||||
f"{foo(bla)}" # OK
|
||||
|
||||
f"{str(bla, 'ascii')}, {str(bla, encoding='cp1255')}" # OK
|
||||
|
||||
@@ -68,6 +68,18 @@ def bad():
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
def fine():
|
||||
try:
|
||||
process()
|
||||
except Exception as e:
|
||||
raise e from None
|
||||
|
||||
def fine():
|
||||
try:
|
||||
process()
|
||||
except Exception as e:
|
||||
raise e from Exception
|
||||
|
||||
def fine():
|
||||
try:
|
||||
process()
|
||||
|
||||
@@ -27,7 +27,7 @@ use ruff_python_semantic::binding::{
|
||||
Binding, BindingId, BindingKind, Exceptions, ExecutionContext, Export, FromImportation,
|
||||
Importation, StarImportation, SubmoduleImportation,
|
||||
};
|
||||
use ruff_python_semantic::context::{Context, ContextFlags};
|
||||
use ruff_python_semantic::context::{Context, ContextFlags, ResolvedReference};
|
||||
use ruff_python_semantic::definition::{ContextualizedDefinition, Module, ModuleKind};
|
||||
use ruff_python_semantic::node::NodeId;
|
||||
use ruff_python_semantic::scope::{ClassDef, FunctionDef, Lambda, Scope, ScopeId, ScopeKind};
|
||||
@@ -35,6 +35,7 @@ use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||
use ruff_python_stdlib::path::is_python_stub_file;
|
||||
|
||||
use crate::checkers::ast::deferred::Deferred;
|
||||
use crate::checkers::ast::traits::RegisteredRule;
|
||||
use crate::docstrings::extraction::ExtractionTarget;
|
||||
use crate::docstrings::Docstring;
|
||||
use crate::fs::relativize_path;
|
||||
@@ -56,6 +57,7 @@ use crate::settings::{flags, Settings};
|
||||
use crate::{autofix, docstrings, noqa, warn_user};
|
||||
|
||||
mod deferred;
|
||||
pub(crate) mod traits;
|
||||
|
||||
pub(crate) struct Checker<'a> {
|
||||
// Settings, static metadata, etc.
|
||||
@@ -77,6 +79,16 @@ pub(crate) struct Checker<'a> {
|
||||
deferred: Deferred<'a>,
|
||||
// Check-specific state.
|
||||
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
|
||||
// Dispatchers
|
||||
call_rules: Vec<RegisteredRule<ast::ExprCall>>,
|
||||
}
|
||||
|
||||
pub(crate) struct RuleContext<'a> {
|
||||
pub(crate) settings: &'a Settings,
|
||||
pub(crate) locator: &'a Locator<'a>,
|
||||
pub(crate) stylist: &'a Stylist<'a>,
|
||||
pub(crate) indexer: &'a Indexer,
|
||||
pub(crate) ctx: &'a Context<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Checker<'a> {
|
||||
@@ -110,10 +122,63 @@ impl<'a> Checker<'a> {
|
||||
diagnostics: Vec::default(),
|
||||
deletions: FxHashSet::default(),
|
||||
flake8_bugbear_seen: Vec::default(),
|
||||
call_rules: [
|
||||
RegisteredRule::new::<flake8_django::rules::DjangoLocalsInRenderFunction>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::DeprecatedUnittestAlias>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::SuperCallWithParameters>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::UnnecessaryEncodeUTF8>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::RedundantOpenModes>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::NativeLiterals>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::OpenAlias>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::ReplaceUniversalNewlines>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::ReplaceStdoutStderr>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::OSErrorAlias>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::NonPEP604Isinstance>(),
|
||||
RegisteredRule::new::<pyupgrade::rules::TypeOfPrimitive>(),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|rule| rule.enabled(settings))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(charlie): Remove these methods from `Checker`, use the immutable `RuleContext` everywhere.
|
||||
impl<'a> RuleContext<'a> {
|
||||
/// Return `true` if a patch should be generated under the given autofix
|
||||
/// `Mode`.
|
||||
pub(crate) fn patch(&self, code: Rule) -> bool {
|
||||
self.settings.rules.should_fix(code)
|
||||
}
|
||||
|
||||
/// Create a [`Generator`] to generate source code based on the current AST state.
|
||||
pub(crate) fn generator(&self) -> Generator {
|
||||
fn quote_style(context: &Context, locator: &Locator, indexer: &Indexer) -> Option<Quote> {
|
||||
if !context.in_f_string() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the quote character used to start the containing f-string.
|
||||
let expr = context.expr()?;
|
||||
let string_range = indexer.f_string_range(expr.start())?;
|
||||
let trailing_quote = trailing_quote(locator.slice(string_range))?;
|
||||
|
||||
// Invert the quote character, if it's a single quote.
|
||||
match *trailing_quote {
|
||||
"'" => Some(Quote::Double),
|
||||
"\"" => Some(Quote::Single),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
Generator::new(
|
||||
self.stylist.indentation(),
|
||||
quote_style(self.ctx, self.locator, self.indexer).unwrap_or(self.stylist.quote()),
|
||||
self.stylist.line_ending(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Checker<'a> {
|
||||
/// Return `true` if a patch should be generated under the given autofix
|
||||
/// `Mode`.
|
||||
@@ -742,6 +807,13 @@ where
|
||||
if self.settings.rules.enabled(Rule::PassInClassBody) {
|
||||
flake8_pyi::rules::pass_in_class_body(self, stmt, body);
|
||||
}
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::EllipsisInNonEmptyClassBody)
|
||||
{
|
||||
flake8_pyi::rules::ellipsis_in_non_empty_class_body(self, stmt, body);
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
@@ -1888,6 +1960,9 @@ where
|
||||
if self.settings.rules.enabled(Rule::InvalidMockAccess) {
|
||||
pygrep_hooks::rules::uncalled_mock_method(self, value);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::NamedExprWithoutContext) {
|
||||
pylint::rules::named_expr_without_context(self, value);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::AsyncioDanglingTask) {
|
||||
if let Some(diagnostic) = ruff::rules::asyncio_dangling_task(value, |expr| {
|
||||
self.ctx.resolve_call_path(expr)
|
||||
@@ -2557,12 +2632,26 @@ where
|
||||
}
|
||||
pandas_vet::rules::attr(self, attr, value, expr);
|
||||
}
|
||||
Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
range: _,
|
||||
}) => {
|
||||
Expr::Call(call) => {
|
||||
let context = RuleContext {
|
||||
settings: self.settings,
|
||||
locator: self.locator,
|
||||
stylist: self.stylist,
|
||||
indexer: self.indexer,
|
||||
ctx: &self.ctx,
|
||||
};
|
||||
for rule in &self.call_rules {
|
||||
rule.run(&mut self.diagnostics, &context, call);
|
||||
}
|
||||
|
||||
// Destructure for the rest of the rules, for now.
|
||||
let ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
range: _,
|
||||
} = call;
|
||||
|
||||
if self.settings.rules.any_enabled(&[
|
||||
// pyflakes
|
||||
Rule::StringDotFormatInvalidFormat,
|
||||
@@ -2662,43 +2751,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// pyupgrade
|
||||
if self.settings.rules.enabled(Rule::TypeOfPrimitive) {
|
||||
pyupgrade::rules::type_of_primitive(self, expr, func, args);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::DeprecatedUnittestAlias) {
|
||||
pyupgrade::rules::deprecated_unittest_alias(self, func);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::SuperCallWithParameters) {
|
||||
pyupgrade::rules::super_call_with_parameters(self, expr, func, args);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::UnnecessaryEncodeUTF8) {
|
||||
pyupgrade::rules::unnecessary_encode_utf8(self, expr, func, args, keywords);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::RedundantOpenModes) {
|
||||
pyupgrade::rules::redundant_open_modes(self, expr);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::NativeLiterals) {
|
||||
pyupgrade::rules::native_literals(self, expr, func, args, keywords);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::OpenAlias) {
|
||||
pyupgrade::rules::open_alias(self, expr, func);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::ReplaceUniversalNewlines) {
|
||||
pyupgrade::rules::replace_universal_newlines(self, func, keywords);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::ReplaceStdoutStderr) {
|
||||
pyupgrade::rules::replace_stdout_stderr(self, expr, func, args, keywords);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::OSErrorAlias) {
|
||||
pyupgrade::rules::os_error_alias_call(self, func);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::NonPEP604Isinstance)
|
||||
&& self.settings.target_version >= PythonVersion::Py310
|
||||
{
|
||||
pyupgrade::rules::use_pep604_isinstance(self, expr, func, args);
|
||||
}
|
||||
|
||||
// flake8-async
|
||||
if self
|
||||
.settings
|
||||
@@ -2846,6 +2898,9 @@ where
|
||||
if self.settings.rules.enabled(Rule::RequestWithoutTimeout) {
|
||||
flake8_bandit::rules::request_without_timeout(self, func, args, keywords);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::ParamikoCall) {
|
||||
flake8_bandit::rules::paramiko_call(self, func);
|
||||
}
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
@@ -3281,15 +3336,6 @@ where
|
||||
{
|
||||
pylint::rules::logging_call(self, func, args, keywords);
|
||||
}
|
||||
|
||||
// flake8-django
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::DjangoLocalsInRenderFunction)
|
||||
{
|
||||
flake8_django::rules::locals_in_render_function(self, func, args, keywords);
|
||||
}
|
||||
}
|
||||
Expr::Dict(ast::ExprDict {
|
||||
keys,
|
||||
@@ -3307,6 +3353,11 @@ where
|
||||
flake8_pie::rules::unnecessary_spread(self, keys, values);
|
||||
}
|
||||
}
|
||||
Expr::Set(ast::ExprSet { elts, range: _ }) => {
|
||||
if self.settings.rules.enabled(Rule::DuplicateValue) {
|
||||
pylint::rules::duplicate_value(self, elts);
|
||||
}
|
||||
}
|
||||
Expr::Yield(_) => {
|
||||
if self.settings.rules.enabled(Rule::YieldOutsideFunction) {
|
||||
pyflakes::rules::yield_outside_function(self, expr);
|
||||
@@ -3342,6 +3393,13 @@ where
|
||||
if self.settings.rules.enabled(Rule::HardcodedSQLExpression) {
|
||||
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
|
||||
}
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::ExplicitFStringTypeConversion)
|
||||
{
|
||||
ruff::rules::explicit_f_string_type_conversion(self, expr, values);
|
||||
}
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
@@ -3841,17 +3899,6 @@ where
|
||||
flake8_simplify::rules::expr_and_false(self, expr);
|
||||
}
|
||||
}
|
||||
Expr::FormattedValue(ast::ExprFormattedValue {
|
||||
value, conversion, ..
|
||||
}) => {
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::ExplicitFStringTypeConversion)
|
||||
{
|
||||
ruff::rules::explicit_f_string_type_conversion(self, value, *conversion);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
@@ -4732,144 +4779,63 @@ impl<'a> Checker<'a> {
|
||||
let Expr::Name(ast::ExprName { id, .. } )= expr else {
|
||||
return;
|
||||
};
|
||||
let id = id.as_str();
|
||||
|
||||
let mut first_iter = true;
|
||||
let mut import_starred = false;
|
||||
|
||||
for scope in self.ctx.scopes.ancestors(self.ctx.scope_id) {
|
||||
if scope.kind.is_class() {
|
||||
if id == "__class__" {
|
||||
return;
|
||||
} else if !first_iter {
|
||||
continue;
|
||||
}
|
||||
match self.ctx.resolve_reference(id, expr.range()) {
|
||||
ResolvedReference::Resolved(..) | ResolvedReference::ImplicitGlobal => {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
if let Some(index) = scope.get(id) {
|
||||
// Mark the binding as used.
|
||||
let context = self.ctx.execution_context();
|
||||
self.ctx.bindings[*index].mark_used(self.ctx.scope_id, expr.range(), context);
|
||||
|
||||
if !self.ctx.in_deferred_type_definition()
|
||||
&& self.ctx.bindings[*index].kind.is_annotation()
|
||||
ResolvedReference::StarImport => {
|
||||
// F405
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::UndefinedLocalWithImportStarUsage)
|
||||
{
|
||||
continue;
|
||||
let sources: Vec<String> = self
|
||||
.ctx
|
||||
.scopes
|
||||
.iter()
|
||||
.flat_map(Scope::star_imports)
|
||||
.map(|StarImportation { level, module }| {
|
||||
helpers::format_import_from(*level, *module)
|
||||
})
|
||||
.sorted()
|
||||
.dedup()
|
||||
.collect();
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedLocalWithImportStarUsage {
|
||||
name: id.to_string(),
|
||||
sources,
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
ResolvedReference::NotFound => {
|
||||
// F821
|
||||
if self.settings.rules.enabled(Rule::UndefinedName) {
|
||||
// Allow __path__.
|
||||
if self.path.ends_with("__init__.py") && id == "__path__" {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the name of the sub-importation is the same as an alias of another
|
||||
// importation and the alias is used, that sub-importation should be
|
||||
// marked as used too.
|
||||
//
|
||||
// This handles code like:
|
||||
// import pyarrow as pa
|
||||
// import pyarrow.csv
|
||||
// print(pa.csv.read_csv("test.csv"))
|
||||
match &self.ctx.bindings[*index].kind {
|
||||
BindingKind::Importation(Importation { name, full_name })
|
||||
| BindingKind::SubmoduleImportation(SubmoduleImportation { name, full_name }) =>
|
||||
// Avoid flagging if `NameError` is handled.
|
||||
if self
|
||||
.ctx
|
||||
.handled_exceptions
|
||||
.iter()
|
||||
.any(|handler_names| handler_names.contains(Exceptions::NAME_ERROR))
|
||||
{
|
||||
let has_alias = full_name
|
||||
.split('.')
|
||||
.last()
|
||||
.map(|segment| &segment != name)
|
||||
.unwrap_or_default();
|
||||
if has_alias {
|
||||
// Mark the sub-importation as used.
|
||||
if let Some(index) = scope.get(full_name) {
|
||||
self.ctx.bindings[*index].mark_used(
|
||||
self.ctx.scope_id,
|
||||
expr.range(),
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
BindingKind::FromImportation(FromImportation { name, full_name }) => {
|
||||
let has_alias = full_name
|
||||
.split('.')
|
||||
.last()
|
||||
.map(|segment| &segment != name)
|
||||
.unwrap_or_default();
|
||||
if has_alias {
|
||||
// Mark the sub-importation as used.
|
||||
if let Some(index) = scope.get(full_name.as_str()) {
|
||||
self.ctx.bindings[*index].mark_used(
|
||||
self.ctx.scope_id,
|
||||
expr.range(),
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedName {
|
||||
name: id.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
first_iter = false;
|
||||
import_starred = import_starred || scope.uses_star_imports();
|
||||
}
|
||||
|
||||
if import_starred {
|
||||
// F405
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::UndefinedLocalWithImportStarUsage)
|
||||
{
|
||||
let sources: Vec<String> = self
|
||||
.ctx
|
||||
.scopes
|
||||
.iter()
|
||||
.flat_map(Scope::star_imports)
|
||||
.map(|StarImportation { level, module }| {
|
||||
helpers::format_import_from(*level, *module)
|
||||
})
|
||||
.sorted()
|
||||
.dedup()
|
||||
.collect();
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedLocalWithImportStarUsage {
|
||||
name: id.to_string(),
|
||||
sources,
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::UndefinedName) {
|
||||
// Allow __path__.
|
||||
if self.path.ends_with("__init__.py") && id == "__path__" {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow "__module__" and "__qualname__" in class scopes.
|
||||
if (id == "__module__" || id == "__qualname__")
|
||||
&& matches!(self.ctx.scope().kind, ScopeKind::Class(..))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid flagging if NameError is handled.
|
||||
if self
|
||||
.ctx
|
||||
.handled_exceptions
|
||||
.iter()
|
||||
.any(|handler_names| handler_names.contains(Exceptions::NAME_ERROR))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedName {
|
||||
name: id.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
crates/ruff/src/checkers/ast/traits.rs
Normal file
42
crates/ruff/src/checkers/ast/traits.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::Settings;
|
||||
|
||||
/// Trait for a lint rule that can be run on an AST node of type `T`.
|
||||
pub(crate) trait Analyzer<T>: Sized {
|
||||
/// The [`Rule`] that this analyzer implements.
|
||||
fn rule() -> Rule;
|
||||
|
||||
/// Run the analyzer on the given node.
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &T);
|
||||
}
|
||||
|
||||
/// Internal representation of a single [`Rule`] that can be run on an AST node of type `T`.
|
||||
pub(super) struct RegisteredRule<T> {
|
||||
rule: Rule,
|
||||
run: Executor<T>,
|
||||
}
|
||||
|
||||
impl<T> RegisteredRule<T> {
|
||||
pub(super) fn new<R: Analyzer<T> + 'static>() -> Self {
|
||||
Self {
|
||||
rule: R::rule(),
|
||||
run: R::run,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn enabled(&self, settings: &Settings) -> bool {
|
||||
settings.rules.enabled(self.rule)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn run(&self, diagnostics: &mut Vec<Diagnostic>, context: &RuleContext, node: &T) {
|
||||
(self.run)(diagnostics, context, node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Executor for an [`Analyzer`] as a generic function pointer.
|
||||
type Executor<T> = fn(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &T);
|
||||
@@ -185,6 +185,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R5501") => (RuleGroup::Unspecified, Rule::CollapsibleElseIf),
|
||||
(Pylint, "W0120") => (RuleGroup::Unspecified, Rule::UselessElseOnLoop),
|
||||
(Pylint, "W0129") => (RuleGroup::Unspecified, Rule::AssertOnStringLiteral),
|
||||
(Pylint, "W0131") => (RuleGroup::Unspecified, Rule::NamedExprWithoutContext),
|
||||
(Pylint, "W0406") => (RuleGroup::Unspecified, Rule::ImportSelf),
|
||||
(Pylint, "W0602") => (RuleGroup::Unspecified, Rule::GlobalVariableNotAssigned),
|
||||
(Pylint, "W0603") => (RuleGroup::Unspecified, Rule::GlobalStatement),
|
||||
@@ -192,6 +193,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "W1508") => (RuleGroup::Unspecified, Rule::InvalidEnvvarDefault),
|
||||
(Pylint, "W2901") => (RuleGroup::Unspecified, Rule::RedefinedLoopName),
|
||||
(Pylint, "W3301") => (RuleGroup::Unspecified, Rule::NestedMinMax),
|
||||
(Pylint, "W0130") => (RuleGroup::Unspecified, Rule::DuplicateValue),
|
||||
|
||||
// flake8-async
|
||||
(Flake8Async, "100") => (RuleGroup::Unspecified, Rule::BlockingHttpCallInAsyncFunction),
|
||||
@@ -507,6 +509,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Bandit, "506") => (RuleGroup::Unspecified, Rule::UnsafeYAMLLoad),
|
||||
(Flake8Bandit, "508") => (RuleGroup::Unspecified, Rule::SnmpInsecureVersion),
|
||||
(Flake8Bandit, "509") => (RuleGroup::Unspecified, Rule::SnmpWeakCryptography),
|
||||
(Flake8Bandit, "601") => (RuleGroup::Unspecified, Rule::ParamikoCall),
|
||||
(Flake8Bandit, "602") => (RuleGroup::Unspecified, Rule::SubprocessPopenWithShellEqualsTrue),
|
||||
(Flake8Bandit, "603") => (RuleGroup::Unspecified, Rule::SubprocessWithoutShellEqualsTrue),
|
||||
(Flake8Bandit, "604") => (RuleGroup::Unspecified, Rule::CallWithShellEqualsTrue),
|
||||
@@ -580,6 +583,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Pyi, "010") => (RuleGroup::Unspecified, Rule::NonEmptyStubBody),
|
||||
(Flake8Pyi, "011") => (RuleGroup::Unspecified, Rule::TypedArgumentDefaultInStub),
|
||||
(Flake8Pyi, "012") => (RuleGroup::Unspecified, Rule::PassInClassBody),
|
||||
(Flake8Pyi, "013") => (RuleGroup::Unspecified, Rule::EllipsisInNonEmptyClassBody),
|
||||
(Flake8Pyi, "014") => (RuleGroup::Unspecified, Rule::ArgumentDefaultInStub),
|
||||
(Flake8Pyi, "015") => (RuleGroup::Unspecified, Rule::AssignmentDefaultInStub),
|
||||
(Flake8Pyi, "016") => (RuleGroup::Unspecified, Rule::DuplicateUnionMember),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use anyhow::{bail, Result};
|
||||
use libcst_native::{
|
||||
Attribute, Call, Comparison, Dict, Expr, Expression, Import, ImportAlias, ImportFrom,
|
||||
ImportNames, Module, SimpleString, SmallStatement, Statement,
|
||||
Attribute, Call, Comparison, Dict, Expr, Expression, FormattedString, FormattedStringContent,
|
||||
FormattedStringExpression, Import, ImportAlias, ImportFrom, ImportNames, Module, Name,
|
||||
SimpleString, SmallStatement, Statement,
|
||||
};
|
||||
|
||||
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
|
||||
@@ -111,3 +112,33 @@ pub(crate) fn match_simple_string<'a, 'b>(
|
||||
bail!("Expected Expression::SimpleString")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_formatted_string<'a, 'b>(
|
||||
expression: &'a mut Expression<'b>,
|
||||
) -> Result<&'a mut FormattedString<'b>> {
|
||||
if let Expression::FormattedString(formatted_string) = expression {
|
||||
Ok(formatted_string)
|
||||
} else {
|
||||
bail!("Expected Expression::FormattedString")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_formatted_string_expression<'a, 'b>(
|
||||
formatted_string_content: &'a mut FormattedStringContent<'b>,
|
||||
) -> Result<&'a mut FormattedStringExpression<'b>> {
|
||||
if let FormattedStringContent::Expression(formatted_string_expression) =
|
||||
formatted_string_content
|
||||
{
|
||||
Ok(formatted_string_expression)
|
||||
} else {
|
||||
bail!("Expected FormattedStringContent::Expression")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_name<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Name<'b>> {
|
||||
if let Expression::Name(name) = expression {
|
||||
Ok(name)
|
||||
} else {
|
||||
bail!("Expected Expression::Name")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,49 +19,60 @@ impl Emitter for JunitEmitter {
|
||||
) -> anyhow::Result<()> {
|
||||
let mut report = Report::new("ruff");
|
||||
|
||||
for (filename, messages) in group_messages_by_filename(messages) {
|
||||
let mut test_suite = TestSuite::new(filename);
|
||||
if messages.is_empty() {
|
||||
let mut test_suite = TestSuite::new("ruff");
|
||||
test_suite
|
||||
.extra
|
||||
.insert("package".to_string(), "org.ruff".to_string());
|
||||
|
||||
for message in messages {
|
||||
let MessageWithLocation {
|
||||
message,
|
||||
start_location,
|
||||
} = message;
|
||||
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
|
||||
status.set_message(message.kind.body.clone());
|
||||
let location = if context.is_jupyter_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
SourceLocation::default()
|
||||
} else {
|
||||
start_location
|
||||
};
|
||||
|
||||
status.set_description(format!(
|
||||
"line {row}, col {col}, {body}",
|
||||
row = location.row,
|
||||
col = location.column,
|
||||
body = message.kind.body
|
||||
));
|
||||
let mut case = TestCase::new(
|
||||
format!("org.ruff.{}", message.kind.rule().noqa_code()),
|
||||
status,
|
||||
);
|
||||
let file_path = Path::new(filename);
|
||||
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
|
||||
let classname = file_path.parent().unwrap().join(file_stem);
|
||||
case.set_classname(classname.to_str().unwrap());
|
||||
case.extra
|
||||
.insert("line".to_string(), location.row.to_string());
|
||||
case.extra
|
||||
.insert("column".to_string(), location.column.to_string());
|
||||
|
||||
test_suite.add_test_case(case);
|
||||
}
|
||||
let mut case = TestCase::new("No errors found", TestCaseStatus::success());
|
||||
case.set_classname("ruff");
|
||||
test_suite.add_test_case(case);
|
||||
report.add_test_suite(test_suite);
|
||||
} else {
|
||||
for (filename, messages) in group_messages_by_filename(messages) {
|
||||
let mut test_suite = TestSuite::new(filename);
|
||||
test_suite
|
||||
.extra
|
||||
.insert("package".to_string(), "org.ruff".to_string());
|
||||
|
||||
for message in messages {
|
||||
let MessageWithLocation {
|
||||
message,
|
||||
start_location,
|
||||
} = message;
|
||||
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
|
||||
status.set_message(message.kind.body.clone());
|
||||
let location = if context.is_jupyter_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
SourceLocation::default()
|
||||
} else {
|
||||
start_location
|
||||
};
|
||||
|
||||
status.set_description(format!(
|
||||
"line {row}, col {col}, {body}",
|
||||
row = location.row,
|
||||
col = location.column,
|
||||
body = message.kind.body
|
||||
));
|
||||
let mut case = TestCase::new(
|
||||
format!("org.ruff.{}", message.kind.rule().noqa_code()),
|
||||
status,
|
||||
);
|
||||
let file_path = Path::new(filename);
|
||||
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
|
||||
let classname = file_path.parent().unwrap().join(file_stem);
|
||||
case.set_classname(classname.to_str().unwrap());
|
||||
case.extra
|
||||
.insert("line".to_string(), location.row.to_string());
|
||||
case.extra
|
||||
.insert("column".to_string(), location.column.to_string());
|
||||
|
||||
test_suite.add_test_case(case);
|
||||
}
|
||||
report.add_test_suite(test_suite);
|
||||
}
|
||||
}
|
||||
|
||||
report.serialize(writer)?;
|
||||
|
||||
@@ -159,7 +159,9 @@ ruff_macros::register_rules!(
|
||||
rules::pylint::rules::LoggingTooManyArgs,
|
||||
rules::pylint::rules::UnexpectedSpecialMethodSignature,
|
||||
rules::pylint::rules::NestedMinMax,
|
||||
rules::pylint::rules::DuplicateValue,
|
||||
rules::pylint::rules::DuplicateBases,
|
||||
rules::pylint::rules::NamedExprWithoutContext,
|
||||
// flake8-async
|
||||
rules::flake8_async::rules::BlockingHttpCallInAsyncFunction,
|
||||
rules::flake8_async::rules::OpenSleepOrSubprocessInAsyncFunction,
|
||||
@@ -422,6 +424,7 @@ ruff_macros::register_rules!(
|
||||
rules::flake8_bandit::rules::HardcodedTempFile,
|
||||
rules::flake8_bandit::rules::HashlibInsecureHashFunction,
|
||||
rules::flake8_bandit::rules::Jinja2AutoescapeFalse,
|
||||
rules::flake8_bandit::rules::ParamikoCall,
|
||||
rules::flake8_bandit::rules::LoggingConfigInsecureListen,
|
||||
rules::flake8_bandit::rules::RequestWithNoCertValidation,
|
||||
rules::flake8_bandit::rules::RequestWithoutTimeout,
|
||||
@@ -510,6 +513,7 @@ ruff_macros::register_rules!(
|
||||
rules::flake8_pyi::rules::BadVersionInfoComparison,
|
||||
rules::flake8_pyi::rules::DocstringInStub,
|
||||
rules::flake8_pyi::rules::DuplicateUnionMember,
|
||||
rules::flake8_pyi::rules::EllipsisInNonEmptyClassBody,
|
||||
rules::flake8_pyi::rules::NonEmptyStubBody,
|
||||
rules::flake8_pyi::rules::PassInClassBody,
|
||||
rules::flake8_pyi::rules::PassStatementStubBody,
|
||||
|
||||
@@ -5,7 +5,7 @@ use rustpython_parser as parser;
|
||||
|
||||
static ALLOWLIST_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r"^(?i)(?:pylint|pyright|noqa|nosec|type:\s*ignore|fmt:\s*(on|off)|isort:\s*(on|off|skip|skip_file|split|dont-add-imports(:\s*\[.*?])?)|mypy:|SPDX-License-Identifier:)"
|
||||
r"^(?i)(?:pylint|pyright|noqa|nosec|region|endregion|type:\s*ignore|fmt:\s*(on|off)|isort:\s*(on|off|skip|skip_file|split|dont-add-imports(:\s*\[.*?])?)|mypy:|SPDX-License-Identifier:)"
|
||||
).unwrap()
|
||||
});
|
||||
static BRACKET_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[()\[\]{}\s]+$").unwrap());
|
||||
@@ -224,6 +224,11 @@ mod tests {
|
||||
assert!(!comment_contains_code("# noqa: A123", &[]));
|
||||
assert!(!comment_contains_code("# noqa:A123", &[]));
|
||||
assert!(!comment_contains_code("# nosec", &[]));
|
||||
assert!(!comment_contains_code("# region", &[]));
|
||||
assert!(!comment_contains_code("# endregion", &[]));
|
||||
assert!(!comment_contains_code("# region.name", &[]));
|
||||
assert!(!comment_contains_code("# region name", &[]));
|
||||
assert!(!comment_contains_code("# region: name", &[]));
|
||||
assert!(!comment_contains_code("# fmt: on", &[]));
|
||||
assert!(!comment_contains_code("# fmt: off", &[]));
|
||||
assert!(!comment_contains_code("# fmt:on", &[]));
|
||||
|
||||
@@ -43,6 +43,7 @@ mod tests {
|
||||
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"); "S112")]
|
||||
#[test_case(Rule::TryExceptPass, Path::new("S110.py"); "S110")]
|
||||
#[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"); "S506")]
|
||||
#[test_case(Rule::ParamikoCall, Path::new("S601.py"); "S601")]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -20,6 +20,7 @@ pub(crate) use jinja2_autoescape_false::{jinja2_autoescape_false, Jinja2Autoesca
|
||||
pub(crate) use logging_config_insecure_listen::{
|
||||
logging_config_insecure_listen, LoggingConfigInsecureListen,
|
||||
};
|
||||
pub(crate) use paramiko_calls::{paramiko_call, ParamikoCall};
|
||||
pub(crate) use request_with_no_cert_validation::{
|
||||
request_with_no_cert_validation, RequestWithNoCertValidation,
|
||||
};
|
||||
@@ -57,6 +58,7 @@ mod hardcoded_tmp_directory;
|
||||
mod hashlib_insecure_hash_functions;
|
||||
mod jinja2_autoescape_false;
|
||||
mod logging_config_insecure_listen;
|
||||
mod paramiko_calls;
|
||||
mod request_with_no_cert_validation;
|
||||
mod request_without_timeout;
|
||||
mod shell_injection;
|
||||
|
||||
31
crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs
Normal file
31
crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
#[violation]
|
||||
pub struct ParamikoCall;
|
||||
|
||||
impl Violation for ParamikoCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Possible shell injection via Paramiko call; check inputs are properly sanitized")
|
||||
}
|
||||
}
|
||||
|
||||
/// S601
|
||||
pub(crate) fn paramiko_call(checker: &mut Checker, func: &Expr) {
|
||||
if checker
|
||||
.ctx
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["paramiko", "exec_command"]
|
||||
})
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(ParamikoCall, func.range()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S601.py:3:1: S601 Possible shell injection via Paramiko call; check inputs are properly sanitized
|
||||
|
|
||||
3 | import paramiko
|
||||
4 |
|
||||
5 | paramiko.exec_command('something; really; unsafe')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ S601
|
||||
|
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
|
||||
use rustpython_parser::ast::{self, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::Rule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of `locals()` in `render` functions.
|
||||
@@ -43,11 +45,25 @@ impl Violation for DjangoLocalsInRenderFunction {
|
||||
}
|
||||
|
||||
/// DJ003
|
||||
impl Analyzer<ast::ExprCall> for DjangoLocalsInRenderFunction {
|
||||
fn rule() -> Rule {
|
||||
Rule::DjangoLocalsInRenderFunction
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, context: &RuleContext, node: &ast::ExprCall) {
|
||||
locals_in_render_function(diagnostics, context, node);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn locals_in_render_function(
|
||||
checker: &mut Checker,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
..
|
||||
}: &ast::ExprCall,
|
||||
) {
|
||||
if !checker
|
||||
.ctx
|
||||
@@ -76,13 +92,13 @@ pub(crate) fn locals_in_render_function(
|
||||
return;
|
||||
};
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
diagnostics.push(Diagnostic::new(
|
||||
DjangoLocalsInRenderFunction,
|
||||
locals.range(),
|
||||
));
|
||||
}
|
||||
|
||||
fn is_locals_call(checker: &Checker, expr: &Expr) -> bool {
|
||||
fn is_locals_call(checker: &RuleContext, expr: &Expr) -> bool {
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
|
||||
return false
|
||||
};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
pub(crate) use all_with_model_form::{all_with_model_form, DjangoAllWithModelForm};
|
||||
pub(crate) use exclude_with_model_form::{exclude_with_model_form, DjangoExcludeWithModelForm};
|
||||
pub(crate) use locals_in_render_function::{
|
||||
locals_in_render_function, DjangoLocalsInRenderFunction,
|
||||
};
|
||||
pub(crate) use locals_in_render_function::DjangoLocalsInRenderFunction;
|
||||
pub(crate) use model_without_dunder_str::{model_without_dunder_str, DjangoModelWithoutDunderStr};
|
||||
pub(crate) use non_leading_receiver_decorator::{
|
||||
non_leading_receiver_decorator, DjangoNonLeadingReceiverDecorator,
|
||||
|
||||
@@ -23,6 +23,8 @@ mod tests {
|
||||
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))]
|
||||
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))]
|
||||
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.pyi"))]
|
||||
#[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.py"))]
|
||||
#[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.pyi"))]
|
||||
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.py"))]
|
||||
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))]
|
||||
#[test_case(Rule::PassInClassBody, Path::new("PYI012.py"))]
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
use rustpython_parser::ast::{Expr, ExprConstant, Ranged, Stmt, StmtExpr};
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::types::RefEquality;
|
||||
|
||||
use crate::autofix::actions::delete_stmt;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Removes ellipses (`...`) in otherwise non-empty class bodies.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// An ellipsis in a class body is only necessary if the class body is
|
||||
/// otherwise empty. If the class body is non-empty, then the ellipsis
|
||||
/// is redundant.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// ...
|
||||
/// value: int
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// value: int
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct EllipsisInNonEmptyClassBody;
|
||||
|
||||
impl Violation for EllipsisInNonEmptyClassBody {
|
||||
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Non-empty class body must not contain `...`")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> Option<String> {
|
||||
Some("Remove unnecessary `...`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI013
|
||||
pub(crate) fn ellipsis_in_non_empty_class_body<'a>(
|
||||
checker: &mut Checker<'a>,
|
||||
parent: &'a Stmt,
|
||||
body: &'a [Stmt],
|
||||
) {
|
||||
// If the class body contains a single statement, then it's fine for it to be an ellipsis.
|
||||
if body.len() == 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
for stmt in body {
|
||||
if let Stmt::Expr(StmtExpr { value, .. }) = &stmt {
|
||||
if let Expr::Constant(ExprConstant { value, .. }) = value.as_ref() {
|
||||
if value.is_ellipsis() {
|
||||
let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range());
|
||||
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let deleted: Vec<&Stmt> =
|
||||
checker.deletions.iter().map(Into::into).collect();
|
||||
let edit = delete_stmt(
|
||||
stmt,
|
||||
Some(parent),
|
||||
&deleted,
|
||||
checker.locator,
|
||||
checker.indexer,
|
||||
checker.stylist,
|
||||
)?;
|
||||
|
||||
// In the unlikely event the class body consists solely of several
|
||||
// consecutive ellipses, `delete_stmt` can actually result in a
|
||||
// `pass`.
|
||||
if edit.is_deletion() || edit.content() == Some("pass") {
|
||||
checker.deletions.insert(RefEquality(stmt));
|
||||
}
|
||||
|
||||
Ok(Fix::automatic(edit))
|
||||
});
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ pub(crate) use bad_version_info_comparison::{
|
||||
};
|
||||
pub(crate) use docstring_in_stubs::{docstring_in_stubs, DocstringInStub};
|
||||
pub(crate) use duplicate_union_member::{duplicate_union_member, DuplicateUnionMember};
|
||||
pub(crate) use ellipsis_in_non_empty_class_body::{
|
||||
ellipsis_in_non_empty_class_body, EllipsisInNonEmptyClassBody,
|
||||
};
|
||||
pub(crate) use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody};
|
||||
pub(crate) use pass_in_class_body::{pass_in_class_body, PassInClassBody};
|
||||
pub(crate) use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody};
|
||||
@@ -24,6 +27,7 @@ pub(crate) use unrecognized_platform::{
|
||||
mod bad_version_info_comparison;
|
||||
mod docstring_in_stubs;
|
||||
mod duplicate_union_member;
|
||||
mod ellipsis_in_non_empty_class_body;
|
||||
mod non_empty_stub_body;
|
||||
mod pass_in_class_body;
|
||||
mod pass_statement_stub_body;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
5 | class OneAttributeClass:
|
||||
6 | value: int
|
||||
7 | ... # Error
|
||||
| ^^^ PYI013
|
||||
8 |
|
||||
9 | class OneAttributeClass2:
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Fix
|
||||
2 2 |
|
||||
3 3 | class OneAttributeClass:
|
||||
4 4 | value: int
|
||||
5 |- ... # Error
|
||||
6 5 |
|
||||
7 6 | class OneAttributeClass2:
|
||||
8 7 | ... # Error
|
||||
|
||||
PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
8 | class OneAttributeClass2:
|
||||
9 | ... # Error
|
||||
| ^^^ PYI013
|
||||
10 | value: int
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Fix
|
||||
5 5 | ... # Error
|
||||
6 6 |
|
||||
7 7 | class OneAttributeClass2:
|
||||
8 |- ... # Error
|
||||
9 8 | value: int
|
||||
10 9 |
|
||||
11 10 | class MyClass:
|
||||
|
||||
PYI013.pyi:12:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
12 | class MyClass:
|
||||
13 | ...
|
||||
| ^^^ PYI013
|
||||
14 | value: int
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Fix
|
||||
9 9 | value: int
|
||||
10 10 |
|
||||
11 11 | class MyClass:
|
||||
12 |- ...
|
||||
13 12 | value: int
|
||||
14 13 |
|
||||
15 14 | class TwoEllipsesClass:
|
||||
|
||||
PYI013.pyi:16:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
16 | class TwoEllipsesClass:
|
||||
17 | ...
|
||||
| ^^^ PYI013
|
||||
18 | ... # Error
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Fix
|
||||
13 13 | value: int
|
||||
14 14 |
|
||||
15 15 | class TwoEllipsesClass:
|
||||
16 |- ...
|
||||
17 16 | ... # Error
|
||||
18 17 |
|
||||
19 18 | class DocstringClass:
|
||||
|
||||
PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
17 | class TwoEllipsesClass:
|
||||
18 | ...
|
||||
19 | ... # Error
|
||||
| ^^^ PYI013
|
||||
20 |
|
||||
21 | class DocstringClass:
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Fix
|
||||
14 14 |
|
||||
15 15 | class TwoEllipsesClass:
|
||||
16 16 | ...
|
||||
17 |- ... # Error
|
||||
17 |+ pass # Error
|
||||
18 18 |
|
||||
19 19 | class DocstringClass:
|
||||
20 20 | """
|
||||
|
||||
PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
24 | """
|
||||
25 |
|
||||
26 | ... # Error
|
||||
| ^^^ PYI013
|
||||
27 |
|
||||
28 | class NonEmptyChild(Exception):
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Fix
|
||||
21 21 | My body only contains an ellipsis.
|
||||
22 22 | """
|
||||
23 23 |
|
||||
24 |- ... # Error
|
||||
25 24 |
|
||||
26 25 | class NonEmptyChild(Exception):
|
||||
27 26 | value: int
|
||||
|
||||
PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
28 | class NonEmptyChild(Exception):
|
||||
29 | value: int
|
||||
30 | ... # Error
|
||||
| ^^^ PYI013
|
||||
31 |
|
||||
32 | class NonEmptyChild2(Exception):
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Fix
|
||||
25 25 |
|
||||
26 26 | class NonEmptyChild(Exception):
|
||||
27 27 | value: int
|
||||
28 |- ... # Error
|
||||
29 28 |
|
||||
30 29 | class NonEmptyChild2(Exception):
|
||||
31 30 | ... # Error
|
||||
|
||||
PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
31 | class NonEmptyChild2(Exception):
|
||||
32 | ... # Error
|
||||
| ^^^ PYI013
|
||||
33 | value: int
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Fix
|
||||
28 28 | ... # Error
|
||||
29 29 |
|
||||
30 30 | class NonEmptyChild2(Exception):
|
||||
31 |- ... # Error
|
||||
32 31 | value: int
|
||||
33 32 |
|
||||
34 33 | class NonEmptyWithInit:
|
||||
|
||||
PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
36 | class NonEmptyWithInit:
|
||||
37 | value: int
|
||||
38 | ... # Error
|
||||
| ^^^ PYI013
|
||||
39 |
|
||||
40 | def __init__():
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Fix
|
||||
33 33 |
|
||||
34 34 | class NonEmptyWithInit:
|
||||
35 35 | value: int
|
||||
36 |- ... # Error
|
||||
37 36 |
|
||||
38 37 | def __init__():
|
||||
39 38 | pass
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ pub(crate) fn convert_for_loop_to_any_all(
|
||||
ReimplementedBuiltin {
|
||||
repl: contents.clone(),
|
||||
},
|
||||
stmt.range(),
|
||||
TextRange::new(stmt.start(), loop_info.terminal),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) && checker.ctx.is_builtin("any") {
|
||||
#[allow(deprecated)]
|
||||
@@ -326,7 +326,7 @@ pub(crate) fn convert_for_loop_to_any_all(
|
||||
ReimplementedBuiltin {
|
||||
repl: contents.clone(),
|
||||
},
|
||||
stmt.range(),
|
||||
TextRange::new(stmt.start(), loop_info.terminal),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) && checker.ctx.is_builtin("all") {
|
||||
#[allow(deprecated)]
|
||||
|
||||
@@ -9,8 +9,8 @@ SIM110.py:3:5: SIM110 [*] Use `return any(check(x) for x in iterable)` instead o
|
||||
| _____^
|
||||
6 | | if check(x):
|
||||
7 | | return True
|
||||
| |_______________________^ SIM110
|
||||
8 | return False
|
||||
8 | | return False
|
||||
| |________________^ SIM110
|
||||
|
|
||||
= help: Replace with `return any(check(x) for x in iterable)`
|
||||
|
||||
@@ -34,8 +34,8 @@ SIM110.py:25:5: SIM110 [*] Use `return all(not check(x) for x in iterable)` inst
|
||||
| _____^
|
||||
28 | | if check(x):
|
||||
29 | | return False
|
||||
| |________________________^ SIM110
|
||||
30 | return True
|
||||
30 | | return True
|
||||
| |_______________^ SIM110
|
||||
|
|
||||
= help: Replace with `return all(not check(x) for x in iterable)`
|
||||
|
||||
@@ -60,8 +60,8 @@ SIM110.py:33:5: SIM110 [*] Use `return all(x.is_empty() for x in iterable)` inst
|
||||
| _____^
|
||||
36 | | if not x.is_empty():
|
||||
37 | | return False
|
||||
| |________________________^ SIM110
|
||||
38 | return True
|
||||
38 | | return True
|
||||
| |_______________^ SIM110
|
||||
|
|
||||
= help: Replace with `return all(x.is_empty() for x in iterable)`
|
||||
|
||||
@@ -200,8 +200,8 @@ SIM110.py:124:5: SIM110 Use `return any(check(x) for x in iterable)` instead of
|
||||
| _____^
|
||||
127 | | if check(x):
|
||||
128 | | return True
|
||||
| |_______________________^ SIM110
|
||||
129 | return False
|
||||
129 | | return False
|
||||
| |________________^ SIM110
|
||||
|
|
||||
= help: Replace with `return any(check(x) for x in iterable)`
|
||||
|
||||
@@ -213,8 +213,8 @@ SIM110.py:134:5: SIM110 Use `return all(not check(x) for x in iterable)` instead
|
||||
| _____^
|
||||
137 | | if check(x):
|
||||
138 | | return False
|
||||
| |________________________^ SIM110
|
||||
139 | return True
|
||||
139 | | return True
|
||||
| |_______________^ SIM110
|
||||
|
|
||||
= help: Replace with `return all(not check(x) for x in iterable)`
|
||||
|
||||
@@ -225,8 +225,8 @@ SIM110.py:144:5: SIM110 [*] Use `return any(check(x) for x in iterable)` instead
|
||||
| _____^
|
||||
146 | | if check(x):
|
||||
147 | | return True
|
||||
| |_______________________^ SIM110
|
||||
148 | return False
|
||||
148 | | return False
|
||||
| |________________^ SIM110
|
||||
|
|
||||
= help: Replace with `return any(check(x) for x in iterable)`
|
||||
|
||||
@@ -250,8 +250,8 @@ SIM110.py:154:5: SIM110 [*] Use `return all(not check(x) for x in iterable)` ins
|
||||
| _____^
|
||||
156 | | if check(x):
|
||||
157 | | return False
|
||||
| |________________________^ SIM110
|
||||
158 | return True
|
||||
158 | | return True
|
||||
| |_______________^ SIM110
|
||||
|
|
||||
= help: Replace with `return all(not check(x) for x in iterable)`
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ SIM111.py:3:5: SIM110 [*] Use `return any(check(x) for x in iterable)` instead o
|
||||
| _____^
|
||||
6 | | if check(x):
|
||||
7 | | return True
|
||||
| |_______________________^ SIM110
|
||||
8 | return False
|
||||
8 | | return False
|
||||
| |________________^ SIM110
|
||||
|
|
||||
= help: Replace with `return any(check(x) for x in iterable)`
|
||||
|
||||
@@ -34,8 +34,8 @@ SIM111.py:25:5: SIM110 [*] Use `return all(not check(x) for x in iterable)` inst
|
||||
| _____^
|
||||
28 | | if check(x):
|
||||
29 | | return False
|
||||
| |________________________^ SIM110
|
||||
30 | return True
|
||||
30 | | return True
|
||||
| |_______________^ SIM110
|
||||
|
|
||||
= help: Replace with `return all(not check(x) for x in iterable)`
|
||||
|
||||
@@ -60,8 +60,8 @@ SIM111.py:33:5: SIM110 [*] Use `return all(x.is_empty() for x in iterable)` inst
|
||||
| _____^
|
||||
36 | | if not x.is_empty():
|
||||
37 | | return False
|
||||
| |________________________^ SIM110
|
||||
38 | return True
|
||||
38 | | return True
|
||||
| |_______________^ SIM110
|
||||
|
|
||||
= help: Replace with `return all(x.is_empty() for x in iterable)`
|
||||
|
||||
@@ -200,8 +200,8 @@ SIM111.py:124:5: SIM110 Use `return any(check(x) for x in iterable)` instead of
|
||||
| _____^
|
||||
127 | | if check(x):
|
||||
128 | | return True
|
||||
| |_______________________^ SIM110
|
||||
129 | return False
|
||||
129 | | return False
|
||||
| |________________^ SIM110
|
||||
|
|
||||
= help: Replace with `return any(check(x) for x in iterable)`
|
||||
|
||||
@@ -213,8 +213,8 @@ SIM111.py:134:5: SIM110 Use `return all(not check(x) for x in iterable)` instead
|
||||
| _____^
|
||||
137 | | if check(x):
|
||||
138 | | return False
|
||||
| |________________________^ SIM110
|
||||
139 | return True
|
||||
139 | | return True
|
||||
| |_______________^ SIM110
|
||||
|
|
||||
= help: Replace with `return all(not check(x) for x in iterable)`
|
||||
|
||||
@@ -225,8 +225,8 @@ SIM111.py:144:5: SIM110 [*] Use `return any(check(x) for x in iterable)` instead
|
||||
| _____^
|
||||
146 | | if check(x):
|
||||
147 | | return True
|
||||
| |_______________________^ SIM110
|
||||
148 | return False
|
||||
148 | | return False
|
||||
| |________________^ SIM110
|
||||
|
|
||||
= help: Replace with `return any(check(x) for x in iterable)`
|
||||
|
||||
@@ -250,8 +250,8 @@ SIM111.py:154:5: SIM110 [*] Use `return all(not check(x) for x in iterable)` ins
|
||||
| _____^
|
||||
156 | | if check(x):
|
||||
157 | | return False
|
||||
| |________________________^ SIM110
|
||||
158 | return True
|
||||
158 | | return True
|
||||
| |_______________^ SIM110
|
||||
|
|
||||
= help: Replace with `return all(not check(x) for x in iterable)`
|
||||
|
||||
@@ -276,8 +276,8 @@ SIM111.py:162:5: SIM110 [*] Use `return all(x in y for x in iterable)` instead o
|
||||
| _____^
|
||||
165 | | if x not in y:
|
||||
166 | | return False
|
||||
| |________________________^ SIM110
|
||||
167 | return True
|
||||
167 | | return True
|
||||
| |_______________^ SIM110
|
||||
|
|
||||
= help: Replace with `return all(x in y for x in iterable)`
|
||||
|
||||
@@ -302,8 +302,8 @@ SIM111.py:170:5: SIM110 [*] Use `return all(x <= y for x in iterable)` instead o
|
||||
| _____^
|
||||
173 | | if x > y:
|
||||
174 | | return False
|
||||
| |________________________^ SIM110
|
||||
175 | return True
|
||||
175 | | return True
|
||||
| |_______________^ SIM110
|
||||
|
|
||||
= help: Replace with `return all(x <= y for x in iterable)`
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ SIM401.py:12:1: SIM401 [*] Use `var = a_dict.get(key, "default2")` instead of an
|
||||
16 | | var = a_dict[key]
|
||||
| |_____________________^ SIM401
|
||||
17 |
|
||||
18 | # SIM401 (default with a complex expression)
|
||||
18 | # OK (default contains effect)
|
||||
|
|
||||
= help: Replace with `var = a_dict.get(key, "default2")`
|
||||
|
||||
@@ -50,7 +50,7 @@ SIM401.py:12:1: SIM401 [*] Use `var = a_dict.get(key, "default2")` instead of an
|
||||
15 |- var = a_dict[key]
|
||||
12 |+var = a_dict.get(key, "default2")
|
||||
16 13 |
|
||||
17 14 | # SIM401 (default with a complex expression)
|
||||
17 14 | # OK (default contains effect)
|
||||
18 15 | if key in a_dict:
|
||||
|
||||
SIM401.py:24:1: SIM401 [*] Use `var = a_dict.get(keys[idx], "default")` instead of an `if` block
|
||||
|
||||
@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E402.py:24:1: E402 Module level import not at top of file
|
||||
|
|
||||
24 | y = x + 1
|
||||
24 | __some__magic = 1
|
||||
25 |
|
||||
26 | import f
|
||||
| ^^^^^^^^ E402
|
||||
|
||||
@@ -36,6 +36,9 @@ mod tests {
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_9.py"); "F401_9")]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_10.py"); "F401_10")]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_11.py"); "F401_11")]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_12.py"); "F401_12")]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_13.py"); "F401_13")]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_14.py"); "F401_14")]
|
||||
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"); "F402")]
|
||||
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"); "F403")]
|
||||
#[test_case(Rule::LateFutureImport, Path::new("F404.py"); "F404")]
|
||||
@@ -470,6 +473,16 @@ mod tests {
|
||||
"#,
|
||||
&[Rule::UndefinedName],
|
||||
);
|
||||
flakes(
|
||||
r#"
|
||||
def f():
|
||||
__qualname__ = 1
|
||||
|
||||
class Foo:
|
||||
__qualname__
|
||||
"#,
|
||||
&[Rule::UnusedVariable],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1148,6 +1161,40 @@ mod tests {
|
||||
"#,
|
||||
&[],
|
||||
);
|
||||
flakes(
|
||||
r#"
|
||||
class Test(object):
|
||||
print(__class__.__name__)
|
||||
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
t = Test()
|
||||
"#,
|
||||
&[Rule::UndefinedName],
|
||||
);
|
||||
flakes(
|
||||
r#"
|
||||
class Test(object):
|
||||
X = [__class__ for _ in range(10)]
|
||||
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
t = Test()
|
||||
"#,
|
||||
&[Rule::UndefinedName],
|
||||
);
|
||||
flakes(
|
||||
r#"
|
||||
def f(self):
|
||||
print(__class__.__name__)
|
||||
self.x = 1
|
||||
|
||||
f()
|
||||
"#,
|
||||
&[Rule::UndefinedName],
|
||||
);
|
||||
}
|
||||
|
||||
/// See: <https://github.com/PyCQA/pyflakes/blob/04ecb0c324ef3b61124e2f80f9e1af6c3a4c7b26/pyflakes/test/test_imports.py>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
@@ -21,7 +21,7 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async.py"); "PLE1142")]
|
||||
#[test_case(Rule::AssertOnStringLiteral, Path::new("assert_on_string_literal.py"); "PLW0129")]
|
||||
#[test_case(Rule::BadStrStripCall, Path::new("bad_str_strip_call.py"); "PLE01310")]
|
||||
#[test_case(Rule::BadStrStripCall, Path::new("bad_str_strip_call.py"); "PLE1310")]
|
||||
#[test_case(Rule::BadStringFormatType, Path::new("bad_string_format_type.py"); "PLE1307")]
|
||||
#[test_case(Rule::BidirectionalUnicode, Path::new("bidirectional_unicode.py"); "PLE2502")]
|
||||
#[test_case(Rule::BinaryOpException, Path::new("binary_op_exception.py"); "PLW0711")]
|
||||
@@ -47,6 +47,7 @@ mod tests {
|
||||
#[test_case(Rule::InvalidAllFormat, Path::new("invalid_all_format.py"); "PLE0605")]
|
||||
#[test_case(Rule::InvalidAllObject, Path::new("invalid_all_object.py"); "PLE0604")]
|
||||
#[test_case(Rule::DuplicateBases, Path::new("duplicate_bases.py"); "PLE0241")]
|
||||
#[test_case(Rule::DuplicateValue, Path::new("duplicate_value.py"); "PLW0130")]
|
||||
#[test_case(Rule::InvalidCharacterBackspace, Path::new("invalid_characters.py"); "PLE2510")]
|
||||
#[test_case(Rule::InvalidCharacterEsc, Path::new("invalid_characters.py"); "PLE2513")]
|
||||
#[test_case(Rule::InvalidCharacterNul, Path::new("invalid_characters.py"); "PLE2514")]
|
||||
@@ -57,6 +58,7 @@ mod tests {
|
||||
#[test_case(Rule::LoggingTooFewArgs, Path::new("logging_too_few_args.py"); "PLE1206")]
|
||||
#[test_case(Rule::LoggingTooManyArgs, Path::new("logging_too_many_args.py"); "PLE1205")]
|
||||
#[test_case(Rule::MagicValueComparison, Path::new("magic_value_comparison.py"); "PLR2004")]
|
||||
#[test_case(Rule::NamedExprWithoutContext, Path::new("named_expr_without_context.py"); "PLW0131")]
|
||||
#[test_case(Rule::NonlocalWithoutBinding, Path::new("nonlocal_without_binding.py"); "PLE0117")]
|
||||
#[test_case(Rule::PropertyWithParameters, Path::new("property_with_parameters.py"); "PLR0206")]
|
||||
#[test_case(Rule::RedefinedLoopName, Path::new("redefined_loop_name.py"); "PLW2901")]
|
||||
|
||||
@@ -4,13 +4,45 @@ use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `else` blocks that consist of a single `if` statement.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// If an `else` block contains a single `if` statement, it can be collapsed
|
||||
/// into an `elif`, thus reducing the indentation level.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def check_sign(value: int) -> None:
|
||||
/// if value > 0:
|
||||
/// print("Number is positive.")
|
||||
/// else:
|
||||
/// if value < 0:
|
||||
/// print("Number is negative.")
|
||||
/// else:
|
||||
/// print("Number is zero.")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def check_sign(value: int) -> None:
|
||||
/// if value > 0:
|
||||
/// print("Number is positive.")
|
||||
/// elif value < 0:
|
||||
/// print("Number is negative.")
|
||||
/// else:
|
||||
/// print("Number is zero.")
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/tutorial/controlflow.html#if-statements)
|
||||
#[violation]
|
||||
pub struct CollapsibleElseIf;
|
||||
|
||||
impl Violation for CollapsibleElseIf {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Consider using `elif` instead of `else` then `if` to remove one indentation level")
|
||||
format!("Use `elif` instead of `else` then `if`, to reduce indentation")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,26 @@ impl fmt::Display for ViolationsCmpop {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for comparisons between constants.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Comparing two constants will always resolve to the same value, so the
|
||||
/// comparison is redundant. Instead, the expression should be replaced
|
||||
/// with the result of the comparison.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// foo = 1 == 1
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// foo = True
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#comparisons)
|
||||
#[violation]
|
||||
pub struct ComparisonOfConstant {
|
||||
left_constant: String,
|
||||
|
||||
@@ -8,6 +8,34 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for duplicate base classes in class definitions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Including duplicate base classes will raise a `TypeError` at runtime.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// class Bar(Foo, Foo):
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// class Bar(Foo):
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/compound_stmts.html#class-definitions)
|
||||
#[violation]
|
||||
pub struct DuplicateBases {
|
||||
base: String,
|
||||
|
||||
57
crates/ruff/src/rules/pylint/rules/duplicate_value.rs
Normal file
57
crates/ruff/src/rules/pylint/rules/duplicate_value.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
use rustpython_parser::ast::{self, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for set literals that contain duplicate values.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// In Python, sets are unordered collections of unique elements. Including a
|
||||
/// duplicate value in a set literal is redundant, and may be indicative of a
|
||||
/// mistake.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// {1, 2, 3, 1}
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// {1, 2, 3}
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct DuplicateValue {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Violation for DuplicateValue {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let DuplicateValue { value } = self;
|
||||
format!("Duplicate value `{value}` in set")
|
||||
}
|
||||
}
|
||||
|
||||
/// PLW0130
|
||||
pub(crate) fn duplicate_value(checker: &mut Checker, elts: &Vec<Expr>) {
|
||||
let mut seen_values: FxHashSet<ComparableExpr> = FxHashSet::default();
|
||||
for elt in elts {
|
||||
if let Expr::Constant(ast::ExprConstant { value, .. }) = elt {
|
||||
let comparable_value: ComparableExpr = elt.into();
|
||||
|
||||
if !seen_values.insert(comparable_value) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
DuplicateValue {
|
||||
value: checker.generator().constant(value),
|
||||
},
|
||||
elt.range(),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,35 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::pylint::settings::ConstantType;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of unnamed numerical constants ("magic") values in
|
||||
/// comparisons.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The use of "magic" can make code harder to read and maintain, as readers
|
||||
/// will have to infer the meaning of the value from the context.
|
||||
///
|
||||
/// For convenience, this rule excludes a variety of common values from the
|
||||
/// "magic" value definition, such as `0`, `1`, `""`, and `"__main__"`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def calculate_discount(price: float) -> float:
|
||||
/// return price * (1 - 0.2)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// DISCOUNT_RATE = 0.2
|
||||
///
|
||||
///
|
||||
/// def calculate_discount(price: float) -> float:
|
||||
/// return price * (1 - DISCOUNT_RATE)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Wikipedia](https://en.wikipedia.org/wiki/Magic_number_(programming)#Unnamed_numerical_constants)
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#constants)
|
||||
#[violation]
|
||||
pub struct MagicValueComparison {
|
||||
value: String,
|
||||
|
||||
@@ -7,6 +7,25 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for submodule imports that are aliased to the submodule name.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using the `from` keyword to import the submodule is more concise and
|
||||
/// readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import concurrent.futures as futures
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from concurrent import futures
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/reference/import.html#submodules)
|
||||
#[violation]
|
||||
pub struct ManualFromImport {
|
||||
module: String,
|
||||
|
||||
@@ -9,6 +9,7 @@ pub(crate) use compare_to_empty_string::{compare_to_empty_string, CompareToEmpty
|
||||
pub(crate) use comparison_of_constant::{comparison_of_constant, ComparisonOfConstant};
|
||||
pub(crate) use continue_in_finally::{continue_in_finally, ContinueInFinally};
|
||||
pub(crate) use duplicate_bases::{duplicate_bases, DuplicateBases};
|
||||
pub(crate) use duplicate_value::{duplicate_value, DuplicateValue};
|
||||
pub(crate) use global_statement::{global_statement, GlobalStatement};
|
||||
pub(crate) use global_variable_not_assigned::GlobalVariableNotAssigned;
|
||||
pub(crate) use import_self::{import_from_self, import_self, ImportSelf};
|
||||
@@ -26,6 +27,7 @@ pub(crate) use load_before_global_declaration::{
|
||||
pub(crate) use logging::{logging_call, LoggingTooFewArgs, LoggingTooManyArgs};
|
||||
pub(crate) use magic_value_comparison::{magic_value_comparison, MagicValueComparison};
|
||||
pub(crate) use manual_import_from::{manual_from_import, ManualFromImport};
|
||||
pub(crate) use named_expr_without_context::{named_expr_without_context, NamedExprWithoutContext};
|
||||
pub(crate) use nested_min_max::{nested_min_max, NestedMinMax};
|
||||
pub(crate) use nonlocal_without_binding::NonlocalWithoutBinding;
|
||||
pub(crate) use property_with_parameters::{property_with_parameters, PropertyWithParameters};
|
||||
@@ -59,6 +61,7 @@ mod compare_to_empty_string;
|
||||
mod comparison_of_constant;
|
||||
mod continue_in_finally;
|
||||
mod duplicate_bases;
|
||||
mod duplicate_value;
|
||||
mod global_statement;
|
||||
mod global_variable_not_assigned;
|
||||
mod import_self;
|
||||
@@ -71,6 +74,7 @@ mod load_before_global_declaration;
|
||||
mod logging;
|
||||
mod magic_value_comparison;
|
||||
mod manual_import_from;
|
||||
mod named_expr_without_context;
|
||||
mod nested_min_max;
|
||||
mod nonlocal_without_binding;
|
||||
mod property_with_parameters;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use rustpython_parser::ast::{self, Expr};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for usages of named expressions (e.g., `a := 42`) that can be
|
||||
/// replaced by regular assignment statements (e.g., `a = 42`).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// While a top-level named expression is syntactically and semantically valid,
|
||||
/// it's less clear than a regular assignment statement. Named expressions are
|
||||
/// intended to be used in comprehensions and generator expressions, where
|
||||
/// assignment statements are not allowed.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// (a := 42)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// a = 42
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct NamedExprWithoutContext;
|
||||
|
||||
impl Violation for NamedExprWithoutContext {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Named expression used without context")
|
||||
}
|
||||
}
|
||||
|
||||
/// PLW0131
|
||||
pub(crate) fn named_expr_without_context(checker: &mut Checker, value: &Expr) {
|
||||
if let Expr::NamedExpr(ast::ExprNamedExpr { range, .. }) = value {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(NamedExprWithoutContext, *range));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,36 @@ use ruff_python_ast::helpers::identifier_range;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for property definitions that accept function parameters.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Properties cannot be called with parameters.
|
||||
///
|
||||
/// If you need to pass parameters to a property, create a method with the
|
||||
/// desired parameters and call that method instead.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Cat:
|
||||
/// @property
|
||||
/// def purr(self, volume):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Cat:
|
||||
/// @property
|
||||
/// def purr(self):
|
||||
/// ...
|
||||
///
|
||||
/// def purr_volume(self, volume):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/library/functions.html#property)
|
||||
#[violation]
|
||||
pub struct PropertyWithParameters;
|
||||
|
||||
|
||||
@@ -8,6 +8,34 @@ use ruff_python_ast::hashable::HashableExpr;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for repeated `isinstance` calls on the same object.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Repeated `isinstance` calls on the same object can be merged into a
|
||||
/// single call.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def is_number(x):
|
||||
/// return isinstance(x, int) or isinstance(x, float) or isinstance(x, complex)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def is_number(x):
|
||||
/// return isinstance(x, (int, float, complex))
|
||||
/// ```
|
||||
///
|
||||
/// Or, for Python 3.10 and later:
|
||||
///
|
||||
/// ```python
|
||||
/// def is_number(x):
|
||||
/// return isinstance(x, int | float | complex)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/library/functions.html#isinstance)
|
||||
#[violation]
|
||||
pub struct RepeatedIsinstanceCalls {
|
||||
obj: String,
|
||||
|
||||
@@ -7,6 +7,35 @@ use crate::autofix::actions::get_or_import_symbol;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of the `exit()` and `quit()`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `exit` and `quit` come from the `site` module, which is typically imported
|
||||
/// automatically during startup. However, it is not _guaranteed_ to be
|
||||
/// imported, and so using these functions may result in a `NameError` at
|
||||
/// runtime. Generally, these constants are intended to be used in an interactive
|
||||
/// interpreter, and not in programs.
|
||||
///
|
||||
/// Prefer `sys.exit()`, as the `sys` module is guaranteed to exist in all
|
||||
/// contexts.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// if __name__ == "__main__":
|
||||
/// exit()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import sys
|
||||
///
|
||||
/// if __name__ == "__main__":
|
||||
/// sys.exit()
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation](https://docs.python.org/3/library/constants.html#constants-added-by-the-site-module)
|
||||
#[violation]
|
||||
pub struct SysExitAlias {
|
||||
name: String,
|
||||
|
||||
@@ -6,6 +6,43 @@ use ruff_python_ast::helpers::identifier_range;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for function definitions that include too many arguments.
|
||||
///
|
||||
/// By default, this rule allows up to five arguments, as configured by the
|
||||
/// `pylint.max-args` option.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Functions with many arguments are harder to understand, maintain, and call.
|
||||
/// Consider refactoring functions with many arguments into smaller functions
|
||||
/// with fewer arguments, or using objects to group related arguments.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def calculate_position(x_pos, y_pos, z_pos, x_vel, y_vel, z_vel, time):
|
||||
/// new_x = x_pos + x_vel * time
|
||||
/// new_y = y_pos + y_vel * time
|
||||
/// new_z = z_pos + z_vel * time
|
||||
/// return new_x, new_y, new_z
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from typing import NamedTuple
|
||||
///
|
||||
///
|
||||
/// class Vector(NamedTuple):
|
||||
/// x: float
|
||||
/// y: float
|
||||
/// z: float
|
||||
///
|
||||
///
|
||||
/// def calculate_position(pos: Vector, vel: Vector, time: float) -> Vector:
|
||||
/// return Vector(*(p + v * time for p, v in zip(pos, vel)))
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `pylint.max-args`
|
||||
#[violation]
|
||||
pub struct TooManyArguments {
|
||||
c_args: usize,
|
||||
|
||||
@@ -5,6 +5,70 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::identifier_range;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for functions or methods with too many branches.
|
||||
///
|
||||
/// By default, this rule allows up to 12 branches. This can be configured
|
||||
/// using the `max-branches` option.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Functions or methods with many branches are harder to understand
|
||||
/// and maintain than functions or methods with fewer branches.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def capital(country):
|
||||
/// if country == "Australia":
|
||||
/// return "Canberra"
|
||||
/// elif country == "Brazil":
|
||||
/// return "Brasilia"
|
||||
/// elif country == "Canada":
|
||||
/// return "Ottawa"
|
||||
/// elif country == "England":
|
||||
/// return "London"
|
||||
/// elif country == "France":
|
||||
/// return "Paris"
|
||||
/// elif country == "Germany":
|
||||
/// return "Berlin"
|
||||
/// elif country == "Poland":
|
||||
/// return "Warsaw"
|
||||
/// elif country == "Romania":
|
||||
/// return "Bucharest"
|
||||
/// elif country == "Spain":
|
||||
/// return "Madrid"
|
||||
/// elif country == "Thailand":
|
||||
/// return "Bangkok"
|
||||
/// elif country == "Turkey":
|
||||
/// return "Ankara"
|
||||
/// elif country == "United States":
|
||||
/// return "Washington"
|
||||
/// else:
|
||||
/// return "Unknown" # 13th branch
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def capital(country):
|
||||
/// capitals = {
|
||||
/// "Australia": "Canberra",
|
||||
/// "Brazil": "Brasilia",
|
||||
/// "Canada": "Ottawa",
|
||||
/// "England": "London",
|
||||
/// "France": "Paris",
|
||||
/// "Germany": "Berlin",
|
||||
/// "Poland": "Warsaw",
|
||||
/// "Romania": "Bucharest",
|
||||
/// "Spain": "Madrid",
|
||||
/// "Thailand": "Bangkok",
|
||||
/// "Turkey": "Ankara",
|
||||
/// "United States": "Washington",
|
||||
/// }
|
||||
/// city = capitals.get(country, "Unknown")
|
||||
/// return city
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Ruff configuration documentation](https://beta.ruff.rs/docs/settings/#max-branches)
|
||||
#[violation]
|
||||
pub struct TooManyBranches {
|
||||
branches: usize,
|
||||
|
||||
@@ -6,6 +6,51 @@ use ruff_python_ast::helpers::{identifier_range, ReturnStatementVisitor};
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for functions or methods with too many return statements.
|
||||
///
|
||||
/// By default, this rule allows up to six return statements, as configured by
|
||||
/// the `pylint.max-returns` option.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Functions or methods with many return statements are harder to understand
|
||||
/// and maintain, and often indicative of complex logic.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def capital(country: str) -> str | None:
|
||||
/// if country == "England":
|
||||
/// return "London"
|
||||
/// elif country == "France":
|
||||
/// return "Paris"
|
||||
/// elif country == "Poland":
|
||||
/// return "Warsaw"
|
||||
/// elif country == "Romania":
|
||||
/// return "Bucharest"
|
||||
/// elif country == "Spain":
|
||||
/// return "Madrid"
|
||||
/// elif country == "Thailand":
|
||||
/// return "Bangkok"
|
||||
/// else:
|
||||
/// return None
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def capital(country: str) -> str | None:
|
||||
/// capitals = {
|
||||
/// "England": "London",
|
||||
/// "France": "Paris",
|
||||
/// "Poland": "Warsaw",
|
||||
/// "Romania": "Bucharest",
|
||||
/// "Spain": "Madrid",
|
||||
/// "Thailand": "Bangkok",
|
||||
/// }
|
||||
/// return capitals.get(country)
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `pylint.max-returns`
|
||||
#[violation]
|
||||
pub struct TooManyReturnStatements {
|
||||
returns: usize,
|
||||
|
||||
@@ -5,6 +5,47 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::identifier_range;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for functions or methods with too many statements.
|
||||
///
|
||||
/// By default, this rule allows up to 50 statements, as configured by the
|
||||
/// `pylint.max-statements` option.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Functions or methods with many statements are harder to understand
|
||||
/// and maintain.
|
||||
///
|
||||
/// Instead, consider refactoring the function or method into smaller
|
||||
/// functions or methods, or identifying generalizable patterns and
|
||||
/// replacing them with generic logic or abstractions.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def is_even(number: int) -> bool:
|
||||
/// if number == 0:
|
||||
/// return True
|
||||
/// elif number == 1:
|
||||
/// return False
|
||||
/// elif number == 2:
|
||||
/// return True
|
||||
/// elif number == 3:
|
||||
/// return False
|
||||
/// elif number == 4:
|
||||
/// return True
|
||||
/// elif number == 5:
|
||||
/// return False
|
||||
/// else:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def is_even(number: int) -> bool:
|
||||
/// return number % 2 == 0
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `pylint.max-statements`
|
||||
#[violation]
|
||||
pub struct TooManyStatements {
|
||||
statements: usize,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
collapsible_else_if.py:38:9: PLR5501 Consider using `elif` instead of `else` then `if` to remove one indentation level
|
||||
collapsible_else_if.py:38:9: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
38 | pass
|
||||
39 | else:
|
||||
@@ -11,7 +11,7 @@ collapsible_else_if.py:38:9: PLR5501 Consider using `elif` instead of `else` the
|
||||
| |________________^ PLR5501
|
||||
|
|
||||
|
||||
collapsible_else_if.py:46:9: PLR5501 Consider using `elif` instead of `else` then `if` to remove one indentation level
|
||||
collapsible_else_if.py:46:9: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
|
|
||||
46 | pass
|
||||
47 | else:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
duplicate_value.py:4:35: PLW0130 Duplicate value `"value1"` in set
|
||||
|
|
||||
4 | # Errors.
|
||||
5 | ###
|
||||
6 | incorrect_set = {"value1", 23, 5, "value1"}
|
||||
| ^^^^^^^^ PLW0130
|
||||
7 | incorrect_set = {1, 1}
|
||||
|
|
||||
|
||||
duplicate_value.py:5:21: PLW0130 Duplicate value `1` in set
|
||||
|
|
||||
5 | ###
|
||||
6 | incorrect_set = {"value1", 23, 5, "value1"}
|
||||
7 | incorrect_set = {1, 1}
|
||||
| ^ PLW0130
|
||||
8 |
|
||||
9 | ###
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
named_expr_without_context.py:2:2: PLW0131 Named expression used without context
|
||||
|
|
||||
2 | # Errors
|
||||
3 | (a := 42)
|
||||
| ^^^^^^^ PLW0131
|
||||
4 | if True:
|
||||
5 | (b := 1)
|
||||
|
|
||||
|
||||
named_expr_without_context.py:4:6: PLW0131 Named expression used without context
|
||||
|
|
||||
4 | (a := 42)
|
||||
5 | if True:
|
||||
6 | (b := 1)
|
||||
| ^^^^^^ PLW0131
|
||||
|
|
||||
|
||||
named_expr_without_context.py:8:6: PLW0131 Named expression used without context
|
||||
|
|
||||
8 | class Foo:
|
||||
9 | (c := 1)
|
||||
| ^^^^^^ PLW0131
|
||||
|
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ use libcst_native::{
|
||||
SmallStatement, Statement, Suite,
|
||||
};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
use rustpython_parser::{lexer, Mode, Tok};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
@@ -51,11 +50,10 @@ pub(crate) fn adjust_indentation(
|
||||
|
||||
/// Generate a fix to remove arguments from a `super` call.
|
||||
pub(crate) fn remove_super_arguments(
|
||||
range: TextRange,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
expr: &Expr,
|
||||
) -> Option<Edit> {
|
||||
let range = expr.range();
|
||||
let contents = locator.slice(range);
|
||||
|
||||
let mut tree = libcst_native::parse_module(contents, None).ok()?;
|
||||
|
||||
@@ -59,6 +59,7 @@ mod tests {
|
||||
#[test_case(Rule::PrintfStringFormatting, Path::new("UP031_1.py"); "UP031_1")]
|
||||
#[test_case(Rule::FString, Path::new("UP032_0.py"); "UP032_0")]
|
||||
#[test_case(Rule::FString, Path::new("UP032_1.py"); "UP032_1")]
|
||||
#[test_case(Rule::FString, Path::new("UP032_2.py"); "UP032_2")]
|
||||
#[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_0.py"); "UP033_0")]
|
||||
#[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_1.py"); "UP033_1")]
|
||||
#[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"); "UP034")]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::ast::{self, Expr, Ranged};
|
||||
use rustpython_parser::ast::{self, Expr};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
#[violation]
|
||||
pub struct DeprecatedUnittestAlias {
|
||||
@@ -27,6 +28,16 @@ impl AlwaysAutofixableViolation for DeprecatedUnittestAlias {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for DeprecatedUnittestAlias {
|
||||
fn rule() -> Rule {
|
||||
Rule::DeprecatedUnittestAlias
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
|
||||
deprecated_unittest_alias(diagnostics, checker, node);
|
||||
}
|
||||
}
|
||||
|
||||
static DEPRECATED_ALIASES: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::new(|| {
|
||||
FxHashMap::from_iter([
|
||||
("failUnlessEqual", "assertEqual"),
|
||||
@@ -48,11 +59,12 @@ static DEPRECATED_ALIASES: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::n
|
||||
});
|
||||
|
||||
/// UP005
|
||||
pub(crate) fn deprecated_unittest_alias(checker: &mut Checker, expr: &Expr) {
|
||||
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = expr else {
|
||||
return;
|
||||
};
|
||||
let Some(target) = DEPRECATED_ALIASES.get(attr.as_str()) else {
|
||||
pub(crate) fn deprecated_unittest_alias(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
ast::ExprCall { func, .. }: &ast::ExprCall,
|
||||
) {
|
||||
let Expr::Attribute(ast::ExprAttribute { value, attr, range, .. }) = func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
|
||||
@@ -61,19 +73,22 @@ pub(crate) fn deprecated_unittest_alias(checker: &mut Checker, expr: &Expr) {
|
||||
if id != "self" {
|
||||
return;
|
||||
}
|
||||
let Some(target) = DEPRECATED_ALIASES.get(attr.as_str()) else {
|
||||
return;
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DeprecatedUnittestAlias {
|
||||
alias: attr.to_string(),
|
||||
target: (*target).to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
*range,
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
format!("self.{target}"),
|
||||
expr.range(),
|
||||
*range,
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ruff_text_size::TextRange;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_format::{
|
||||
@@ -7,6 +9,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_ast::str::{is_implicit_concatenation, leading_quote, trailing_quote};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -34,21 +37,20 @@ impl AlwaysAutofixableViolation for FString {
|
||||
/// respectively.
|
||||
#[derive(Debug)]
|
||||
struct FormatSummaryValues<'a> {
|
||||
args: Vec<String>,
|
||||
kwargs: FxHashMap<&'a str, String>,
|
||||
args: Vec<&'a Expr>,
|
||||
kwargs: FxHashMap<&'a str, &'a Expr>,
|
||||
}
|
||||
|
||||
impl<'a> FormatSummaryValues<'a> {
|
||||
fn try_from_expr(checker: &'a Checker, expr: &'a Expr) -> Option<Self> {
|
||||
let mut extracted_args: Vec<String> = Vec::new();
|
||||
let mut extracted_kwargs: FxHashMap<&str, String> = FxHashMap::default();
|
||||
fn try_from_expr(expr: &'a Expr, locator: &'a Locator) -> Option<Self> {
|
||||
let mut extracted_args: Vec<&Expr> = Vec::new();
|
||||
let mut extracted_kwargs: FxHashMap<&str, &Expr> = FxHashMap::default();
|
||||
if let Expr::Call(ast::ExprCall { args, keywords, .. }) = expr {
|
||||
for arg in args {
|
||||
let arg = checker.locator.slice(arg.range());
|
||||
if contains_invalids(arg) {
|
||||
if contains_invalids(locator.slice(arg.range())) {
|
||||
return None;
|
||||
}
|
||||
extracted_args.push(arg.to_string());
|
||||
extracted_args.push(arg);
|
||||
}
|
||||
for keyword in keywords {
|
||||
let Keyword {
|
||||
@@ -57,11 +59,10 @@ impl<'a> FormatSummaryValues<'a> {
|
||||
range: _,
|
||||
} = keyword;
|
||||
if let Some(key) = arg {
|
||||
let kwarg = checker.locator.slice(value.range());
|
||||
if contains_invalids(kwarg) {
|
||||
if contains_invalids(locator.slice(value.range())) {
|
||||
return None;
|
||||
}
|
||||
extracted_kwargs.insert(key, kwarg.to_string());
|
||||
extracted_kwargs.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +77,7 @@ impl<'a> FormatSummaryValues<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
fn consume_next(&mut self) -> Option<String> {
|
||||
fn consume_next(&mut self) -> Option<&Expr> {
|
||||
if self.args.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -84,7 +85,7 @@ impl<'a> FormatSummaryValues<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_arg(&mut self, index: usize) -> Option<String> {
|
||||
fn consume_arg(&mut self, index: usize) -> Option<&Expr> {
|
||||
if self.args.len() > index {
|
||||
Some(self.args.remove(index))
|
||||
} else {
|
||||
@@ -92,13 +93,13 @@ impl<'a> FormatSummaryValues<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_kwarg(&mut self, key: &str) -> Option<String> {
|
||||
fn consume_kwarg(&mut self, key: &str) -> Option<&Expr> {
|
||||
self.kwargs.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the string contains characters that are forbidden in
|
||||
/// argument identifier.
|
||||
/// Return `true` if the string contains characters that are forbidden by
|
||||
/// argument identifiers.
|
||||
fn contains_invalids(string: &str) -> bool {
|
||||
string.contains('*')
|
||||
|| string.contains('\'')
|
||||
@@ -106,8 +107,61 @@ fn contains_invalids(string: &str) -> bool {
|
||||
|| string.contains("await")
|
||||
}
|
||||
|
||||
enum FormatContext {
|
||||
/// The expression is used as a bare format spec (e.g., `{x}`).
|
||||
Bare,
|
||||
/// The expression is used with conversion flags, or attribute or subscript access
|
||||
/// (e.g., `{x!r}`, `{x.y}`, `{x[y]}`).
|
||||
Accessed,
|
||||
}
|
||||
|
||||
/// Given an [`Expr`], format it for use in a formatted expression within an f-string.
|
||||
fn formatted_expr<'a>(expr: &Expr, context: FormatContext, locator: &Locator<'a>) -> Cow<'a, str> {
|
||||
let text = locator.slice(expr.range());
|
||||
let parenthesize = match (context, expr) {
|
||||
// E.g., `x + y` should be parenthesized in `f"{(x + y)[0]}"`.
|
||||
(
|
||||
FormatContext::Accessed,
|
||||
Expr::BinOp(_)
|
||||
| Expr::UnaryOp(_)
|
||||
| Expr::BoolOp(_)
|
||||
| Expr::NamedExpr(_)
|
||||
| Expr::Compare(_)
|
||||
| Expr::IfExp(_)
|
||||
| Expr::Lambda(_)
|
||||
| Expr::Await(_)
|
||||
| Expr::Yield(_)
|
||||
| Expr::YieldFrom(_)
|
||||
| Expr::Starred(_),
|
||||
) => true,
|
||||
// E.g., `12` should be parenthesized in `f"{(12).real}"`.
|
||||
(
|
||||
FormatContext::Accessed,
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(..),
|
||||
..
|
||||
}),
|
||||
) => text.chars().all(|c| c.is_ascii_digit()),
|
||||
// E.g., `{x, y}` should be parenthesized in `f"{(x, y)}"`.
|
||||
(
|
||||
_,
|
||||
Expr::GeneratorExp(_)
|
||||
| Expr::Dict(_)
|
||||
| Expr::Set(_)
|
||||
| Expr::SetComp(_)
|
||||
| Expr::DictComp(_),
|
||||
) => true,
|
||||
_ => false,
|
||||
};
|
||||
if parenthesize && !text.starts_with('(') && !text.ends_with(')') {
|
||||
Cow::Owned(format!("({text})"))
|
||||
} else {
|
||||
Cow::Borrowed(text)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an f-string from an [`Expr`].
|
||||
fn try_convert_to_f_string(checker: &Checker, expr: &Expr) -> Option<String> {
|
||||
fn try_convert_to_f_string(expr: &Expr, locator: &Locator) -> Option<String> {
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
|
||||
return None;
|
||||
};
|
||||
@@ -124,11 +178,11 @@ fn try_convert_to_f_string(checker: &Checker, expr: &Expr) -> Option<String> {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Some(mut summary) = FormatSummaryValues::try_from_expr(checker, expr) else {
|
||||
let Some(mut summary) = FormatSummaryValues::try_from_expr( expr, locator) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let contents = checker.locator.slice(value.range());
|
||||
let contents = locator.slice(value.range());
|
||||
|
||||
// Skip implicit string concatenations.
|
||||
if is_implicit_concatenation(contents) {
|
||||
@@ -171,26 +225,20 @@ fn try_convert_to_f_string(checker: &Checker, expr: &Expr) -> Option<String> {
|
||||
converted.push('{');
|
||||
|
||||
let field = FieldName::parse(&field_name).ok()?;
|
||||
match field.field_type {
|
||||
FieldType::Auto => {
|
||||
let Some(arg) = summary.consume_next() else {
|
||||
return None;
|
||||
};
|
||||
converted.push_str(&arg);
|
||||
}
|
||||
FieldType::Index(index) => {
|
||||
let Some(arg) = summary.consume_arg(index) else {
|
||||
return None;
|
||||
};
|
||||
converted.push_str(&arg);
|
||||
}
|
||||
FieldType::Keyword(name) => {
|
||||
let Some(arg) = summary.consume_kwarg(&name) else {
|
||||
return None;
|
||||
};
|
||||
converted.push_str(&arg);
|
||||
}
|
||||
}
|
||||
let arg = match field.field_type {
|
||||
FieldType::Auto => summary.consume_next(),
|
||||
FieldType::Index(index) => summary.consume_arg(index),
|
||||
FieldType::Keyword(name) => summary.consume_kwarg(&name),
|
||||
}?;
|
||||
converted.push_str(&formatted_expr(
|
||||
arg,
|
||||
if field.parts.is_empty() {
|
||||
FormatContext::Bare
|
||||
} else {
|
||||
FormatContext::Accessed
|
||||
},
|
||||
locator,
|
||||
));
|
||||
|
||||
for part in field.parts {
|
||||
match part {
|
||||
@@ -258,7 +306,7 @@ pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &E
|
||||
|
||||
// Currently, the only issue we know of is in LibCST:
|
||||
// https://github.com/Instagram/LibCST/issues/846
|
||||
let Some(mut contents) = try_convert_to_f_string(checker, expr) else {
|
||||
let Some(mut contents) = try_convert_to_f_string( expr, checker.locator) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ pub(crate) use deprecated_import::{deprecated_import, DeprecatedImport};
|
||||
pub(crate) use deprecated_mock_import::{
|
||||
deprecated_mock_attribute, deprecated_mock_import, DeprecatedMockImport,
|
||||
};
|
||||
pub(crate) use deprecated_unittest_alias::{deprecated_unittest_alias, DeprecatedUnittestAlias};
|
||||
pub(crate) use deprecated_unittest_alias::DeprecatedUnittestAlias;
|
||||
pub(crate) use extraneous_parentheses::{extraneous_parentheses, ExtraneousParentheses};
|
||||
pub(crate) use f_strings::{f_strings, FString};
|
||||
pub(crate) use format_literals::{format_literals, FormatLiterals};
|
||||
@@ -20,31 +20,29 @@ pub(crate) use lru_cache_with_maxsize_none::{
|
||||
pub(crate) use lru_cache_without_parameters::{
|
||||
lru_cache_without_parameters, LRUCacheWithoutParameters,
|
||||
};
|
||||
pub(crate) use native_literals::{native_literals, NativeLiterals};
|
||||
pub(crate) use open_alias::{open_alias, OpenAlias};
|
||||
pub(crate) use os_error_alias::{
|
||||
os_error_alias_call, os_error_alias_handlers, os_error_alias_raise, OSErrorAlias,
|
||||
};
|
||||
pub(crate) use native_literals::NativeLiterals;
|
||||
pub(crate) use open_alias::OpenAlias;
|
||||
pub(crate) use os_error_alias::{os_error_alias_handlers, os_error_alias_raise, OSErrorAlias};
|
||||
pub(crate) use outdated_version_block::{outdated_version_block, OutdatedVersionBlock};
|
||||
pub(crate) use printf_string_formatting::{printf_string_formatting, PrintfStringFormatting};
|
||||
pub(crate) use quoted_annotation::{quoted_annotation, QuotedAnnotation};
|
||||
pub(crate) use redundant_open_modes::{redundant_open_modes, RedundantOpenModes};
|
||||
pub(crate) use replace_stdout_stderr::{replace_stdout_stderr, ReplaceStdoutStderr};
|
||||
pub(crate) use replace_universal_newlines::{replace_universal_newlines, ReplaceUniversalNewlines};
|
||||
pub(crate) use super_call_with_parameters::{super_call_with_parameters, SuperCallWithParameters};
|
||||
pub(crate) use type_of_primitive::{type_of_primitive, TypeOfPrimitive};
|
||||
pub(crate) use redundant_open_modes::RedundantOpenModes;
|
||||
pub(crate) use replace_stdout_stderr::ReplaceStdoutStderr;
|
||||
pub(crate) use replace_universal_newlines::ReplaceUniversalNewlines;
|
||||
pub(crate) use super_call_with_parameters::SuperCallWithParameters;
|
||||
pub(crate) use type_of_primitive::TypeOfPrimitive;
|
||||
pub(crate) use typing_text_str_alias::{typing_text_str_alias, TypingTextStrAlias};
|
||||
pub(crate) use unicode_kind_prefix::{unicode_kind_prefix, UnicodeKindPrefix};
|
||||
pub(crate) use unnecessary_builtin_import::{unnecessary_builtin_import, UnnecessaryBuiltinImport};
|
||||
pub(crate) use unnecessary_coding_comment::{unnecessary_coding_comment, UTF8EncodingDeclaration};
|
||||
pub(crate) use unnecessary_encode_utf8::{unnecessary_encode_utf8, UnnecessaryEncodeUTF8};
|
||||
pub(crate) use unnecessary_encode_utf8::UnnecessaryEncodeUTF8;
|
||||
pub(crate) use unnecessary_future_import::{unnecessary_future_import, UnnecessaryFutureImport};
|
||||
pub(crate) use unpacked_list_comprehension::{
|
||||
unpacked_list_comprehension, UnpackedListComprehension,
|
||||
};
|
||||
pub(crate) use use_pep585_annotation::{use_pep585_annotation, NonPEP585Annotation};
|
||||
pub(crate) use use_pep604_annotation::{use_pep604_annotation, NonPEP604Annotation};
|
||||
pub(crate) use use_pep604_isinstance::{use_pep604_isinstance, NonPEP604Isinstance};
|
||||
pub(crate) use use_pep604_isinstance::NonPEP604Isinstance;
|
||||
pub(crate) use useless_metaclass_type::{useless_metaclass_type, UselessMetaclassType};
|
||||
pub(crate) use useless_object_inheritance::{useless_object_inheritance, UselessObjectInheritance};
|
||||
pub(crate) use yield_in_for_loop::{yield_in_for_loop, YieldInForLoop};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::fmt;
|
||||
|
||||
use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
|
||||
use rustpython_parser::ast::{self, Constant, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::str::is_implicit_concatenation;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub(crate) enum LiteralType {
|
||||
@@ -43,15 +43,30 @@ impl AlwaysAutofixableViolation for NativeLiterals {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for NativeLiterals {
|
||||
fn rule() -> Rule {
|
||||
Rule::NativeLiterals
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
|
||||
native_literals(diagnostics, checker, node);
|
||||
}
|
||||
}
|
||||
|
||||
/// UP018
|
||||
pub(crate) fn native_literals(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
range,
|
||||
}: &ast::ExprCall,
|
||||
) {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func else { return; };
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !keywords.is_empty() || args.len() > 1 {
|
||||
return;
|
||||
@@ -63,7 +78,7 @@ pub(crate) fn native_literals(
|
||||
LiteralType::Str
|
||||
} else {
|
||||
LiteralType::Bytes
|
||||
}}, expr.range());
|
||||
}}, *range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let constant = if id == "bytes" {
|
||||
Constant::Bytes(vec![])
|
||||
@@ -74,10 +89,10 @@ pub(crate) fn native_literals(
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
content,
|
||||
expr.range(),
|
||||
*range,
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -121,15 +136,15 @@ pub(crate) fn native_literals(
|
||||
LiteralType::Bytes
|
||||
},
|
||||
},
|
||||
expr.range(),
|
||||
*range,
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
arg_code.to_string(),
|
||||
expr.range(),
|
||||
*range,
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use rustpython_parser::ast::{Expr, Ranged};
|
||||
use rustpython_parser::ast::{self, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
#[violation]
|
||||
pub struct OpenAlias;
|
||||
@@ -22,25 +23,39 @@ impl Violation for OpenAlias {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for OpenAlias {
|
||||
fn rule() -> Rule {
|
||||
Rule::OpenAlias
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, context: &RuleContext, node: &ast::ExprCall) {
|
||||
open_alias(diagnostics, context, node);
|
||||
}
|
||||
}
|
||||
|
||||
/// UP020
|
||||
pub(crate) fn open_alias(checker: &mut Checker, expr: &Expr, func: &Expr) {
|
||||
if checker
|
||||
pub(crate) fn open_alias(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
context: &RuleContext,
|
||||
ast::ExprCall { func, range, .. }: &ast::ExprCall,
|
||||
) {
|
||||
if context
|
||||
.ctx
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["io", "open"])
|
||||
{
|
||||
let fixable = checker
|
||||
let fixable = context
|
||||
.ctx
|
||||
.find_binding("open")
|
||||
.map_or(true, |binding| binding.kind.is_builtin());
|
||||
let mut diagnostic = Diagnostic::new(OpenAlias, expr.range());
|
||||
if fixable && checker.patch(diagnostic.kind.rule()) {
|
||||
let mut diagnostic = Diagnostic::new(OpenAlias, *range);
|
||||
if fixable && context.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
"open".to_string(),
|
||||
func.range(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ use rustpython_parser::ast::{self, Excepthandler, Expr, ExprContext, Ranged};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::compose_call_path;
|
||||
|
||||
use ruff_python_semantic::context::Context;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::{Checker, RuleContext};
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
#[violation]
|
||||
pub struct OSErrorAlias {
|
||||
@@ -30,6 +30,16 @@ impl AlwaysAutofixableViolation for OSErrorAlias {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for OSErrorAlias {
|
||||
fn rule() -> Rule {
|
||||
Rule::OSErrorAlias
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
|
||||
os_error_alias_call(diagnostics, checker, node);
|
||||
}
|
||||
}
|
||||
|
||||
const ALIASES: &[(&str, &str)] = &[
|
||||
("", "EnvironmentError"),
|
||||
("", "IOError"),
|
||||
@@ -73,6 +83,24 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`].
|
||||
fn immutable_atom_diagnostic(checker: &RuleContext, target: &Expr) -> Diagnostic {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
OSErrorAlias {
|
||||
name: compose_call_path(target),
|
||||
},
|
||||
target.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
"OSError".to_string(),
|
||||
target.range(),
|
||||
)));
|
||||
}
|
||||
diagnostic
|
||||
}
|
||||
|
||||
/// Create a [`Diagnostic`] for a tuple of expressions.
|
||||
fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) {
|
||||
let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, target.range());
|
||||
@@ -156,9 +184,13 @@ pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[Excepth
|
||||
}
|
||||
|
||||
/// UP024
|
||||
pub(crate) fn os_error_alias_call(checker: &mut Checker, func: &Expr) {
|
||||
if is_alias(&checker.ctx, func) {
|
||||
atom_diagnostic(checker, func);
|
||||
pub(crate) fn os_error_alias_call(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
ast::ExprCall { func, .. }: &ast::ExprCall,
|
||||
) {
|
||||
if is_alias(checker.ctx, func) {
|
||||
diagnostics.push(immutable_atom_diagnostic(checker, func));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_text_size::TextRange;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruff_text_size::TextRange;
|
||||
use rustpython_format::cformat::{
|
||||
CConversionFlags, CFormatPart, CFormatPrecision, CFormatQuantity, CFormatString,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
|
||||
@@ -10,7 +10,8 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::find_keyword;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::Rule;
|
||||
|
||||
#[violation]
|
||||
@@ -41,6 +42,16 @@ impl AlwaysAutofixableViolation for RedundantOpenModes {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for RedundantOpenModes {
|
||||
fn rule() -> Rule {
|
||||
Rule::RedundantOpenModes
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
|
||||
redundant_open_modes(diagnostics, checker, node);
|
||||
}
|
||||
}
|
||||
|
||||
const OPEN_FUNC_NAME: &str = "open";
|
||||
const MODE_KEYWORD_ARGUMENT: &str = "mode";
|
||||
|
||||
@@ -86,24 +97,24 @@ impl OpenMode {
|
||||
}
|
||||
}
|
||||
|
||||
fn match_open(expr: &Expr) -> (Option<&Expr>, Vec<Keyword>) {
|
||||
if let Expr::Call(ast::ExprCall {
|
||||
fn match_open(
|
||||
ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
range: _,
|
||||
}) = expr
|
||||
{
|
||||
if matches!(func.as_ref(), Expr::Name(ast::ExprName {id, ..}) if id == OPEN_FUNC_NAME) {
|
||||
// Return the "open mode" parameter and keywords.
|
||||
return (args.get(1), keywords.clone());
|
||||
}
|
||||
}: &ast::ExprCall,
|
||||
) -> (Option<&Expr>, Vec<Keyword>) {
|
||||
if matches!(func.as_ref(), Expr::Name(ast::ExprName {id, ..}) if id == OPEN_FUNC_NAME) {
|
||||
// Return the "open mode" parameter and keywords.
|
||||
(args.get(1), keywords.clone())
|
||||
} else {
|
||||
(None, vec![])
|
||||
}
|
||||
(None, vec![])
|
||||
}
|
||||
|
||||
fn create_check(
|
||||
expr: &Expr,
|
||||
expr: &ast::ExprCall,
|
||||
mode_param: &Expr,
|
||||
replacement_value: Option<&str>,
|
||||
locator: &Locator,
|
||||
@@ -130,7 +141,11 @@ fn create_check(
|
||||
diagnostic
|
||||
}
|
||||
|
||||
fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) -> Result<Edit> {
|
||||
fn create_remove_param_fix(
|
||||
locator: &Locator,
|
||||
expr: &ast::ExprCall,
|
||||
mode_param: &Expr,
|
||||
) -> Result<Edit> {
|
||||
let content = locator.slice(expr.range());
|
||||
// Find the last comma before mode_param and create a deletion fix
|
||||
// starting from the comma and ending after mode_param.
|
||||
@@ -171,7 +186,11 @@ fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) ->
|
||||
}
|
||||
|
||||
/// UP015
|
||||
pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
|
||||
pub(crate) fn redundant_open_modes(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
expr: &ast::ExprCall,
|
||||
) {
|
||||
// If `open` has been rebound, skip this check entirely.
|
||||
if !checker.ctx.is_builtin(OPEN_FUNC_NAME) {
|
||||
return;
|
||||
@@ -185,7 +204,7 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
|
||||
}) = &keyword.value
|
||||
{
|
||||
if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) {
|
||||
checker.diagnostics.push(create_check(
|
||||
diagnostics.push(create_check(
|
||||
expr,
|
||||
&keyword.value,
|
||||
mode.replacement_value(),
|
||||
@@ -202,7 +221,7 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
|
||||
}) = &mode_param
|
||||
{
|
||||
if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) {
|
||||
checker.diagnostics.push(create_check(
|
||||
diagnostics.push(create_check(
|
||||
expr,
|
||||
mode_param,
|
||||
mode.replacement_value(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::{Expr, Keyword, Ranged};
|
||||
use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
@@ -7,8 +7,9 @@ use ruff_python_ast::helpers::find_keyword;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
|
||||
use crate::autofix::actions::remove_argument;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
#[violation]
|
||||
pub struct ReplaceStdoutStderr;
|
||||
@@ -24,6 +25,16 @@ impl AlwaysAutofixableViolation for ReplaceStdoutStderr {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for ReplaceStdoutStderr {
|
||||
fn rule() -> Rule {
|
||||
Rule::ReplaceStdoutStderr
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
|
||||
replace_stdout_stderr(diagnostics, checker, node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a [`Edit`] for a `stdout` and `stderr` [`Keyword`] pair.
|
||||
fn generate_fix(
|
||||
locator: &Locator,
|
||||
@@ -54,11 +65,14 @@ fn generate_fix(
|
||||
|
||||
/// UP022
|
||||
pub(crate) fn replace_stdout_stderr(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
range,
|
||||
}: &ast::ExprCall,
|
||||
) {
|
||||
if checker
|
||||
.ctx
|
||||
@@ -92,12 +106,12 @@ pub(crate) fn replace_stdout_stderr(
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(ReplaceStdoutStderr, expr.range());
|
||||
let mut diagnostic = Diagnostic::new(ReplaceStdoutStderr, *range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
generate_fix(checker.locator, func, args, keywords, stdout, stderr)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use ruff_text_size::{TextLen, TextRange};
|
||||
use rustpython_parser::ast::{Expr, Keyword, Ranged};
|
||||
use rustpython_parser::ast::{self, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::find_keyword;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
#[violation]
|
||||
pub struct ReplaceUniversalNewlines;
|
||||
@@ -22,8 +23,22 @@ impl AlwaysAutofixableViolation for ReplaceUniversalNewlines {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for ReplaceUniversalNewlines {
|
||||
fn rule() -> Rule {
|
||||
Rule::ReplaceUniversalNewlines
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
|
||||
replace_universal_newlines(diagnostics, checker, node);
|
||||
}
|
||||
}
|
||||
|
||||
/// UP021
|
||||
pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwargs: &[Keyword]) {
|
||||
pub(crate) fn replace_universal_newlines(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
ast::ExprCall { func, keywords, .. }: &ast::ExprCall,
|
||||
) {
|
||||
if checker
|
||||
.ctx
|
||||
.resolve_call_path(func)
|
||||
@@ -31,8 +46,8 @@ pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwa
|
||||
call_path.as_slice() == ["subprocess", "run"]
|
||||
})
|
||||
{
|
||||
let Some(kwarg) = find_keyword(kwargs, "universal_newlines") else { return; };
|
||||
let range = TextRange::at(kwarg.start(), "universal_newlines".text_len());
|
||||
let Some(keyword) = find_keyword(keywords, "universal_newlines") else { return; };
|
||||
let range = TextRange::at(keyword.start(), "universal_newlines".text_len());
|
||||
let mut diagnostic = Diagnostic::new(ReplaceUniversalNewlines, range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
@@ -41,6 +56,6 @@ pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwa
|
||||
range,
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use rustpython_parser::ast::{self, Arg, Expr, Ranged, Stmt};
|
||||
use rustpython_parser::ast::{self, Arg, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::scope::ScopeKind;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::pyupgrade::fixes;
|
||||
|
||||
#[violation]
|
||||
@@ -22,6 +23,16 @@ impl AlwaysAutofixableViolation for SuperCallWithParameters {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for SuperCallWithParameters {
|
||||
fn rule() -> Rule {
|
||||
Rule::SuperCallWithParameters
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
|
||||
super_call_with_parameters(diagnostics, checker, node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if a call is an argumented `super` invocation.
|
||||
fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = func {
|
||||
@@ -33,10 +44,11 @@ fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
|
||||
|
||||
/// UP008
|
||||
pub(crate) fn super_call_with_parameters(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
ast::ExprCall {
|
||||
func, args, range, ..
|
||||
}: &ast::ExprCall,
|
||||
) {
|
||||
// Only bother going through the super check at all if we're in a `super` call.
|
||||
// (We check this in `super_args` too, so this is just an optimization.)
|
||||
@@ -55,7 +67,7 @@ pub(crate) fn super_call_with_parameters(
|
||||
// For a `super` invocation to be unnecessary, the first argument needs to match
|
||||
// the enclosing class, and the second argument needs to match the first
|
||||
// argument to the enclosing function.
|
||||
let [first_arg, second_arg] = args else {
|
||||
let [first_arg, second_arg] = args.as_slice() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -97,12 +109,13 @@ pub(crate) fn super_call_with_parameters(
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(SuperCallWithParameters, expr.range());
|
||||
let mut diagnostic = Diagnostic::new(SuperCallWithParameters, *range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(edit) = fixes::remove_super_arguments(checker.locator, checker.stylist, expr) {
|
||||
if let Some(edit) = fixes::remove_super_arguments(*range, checker.locator, checker.stylist)
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(edit));
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use rustpython_parser::ast::{self, Expr, Ranged};
|
||||
use rustpython_parser::ast::{self, Expr};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
use super::super::types::Primitive;
|
||||
|
||||
@@ -26,8 +27,24 @@ impl AlwaysAutofixableViolation for TypeOfPrimitive {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for TypeOfPrimitive {
|
||||
fn rule() -> Rule {
|
||||
Rule::TypeOfPrimitive
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
|
||||
type_of_primitive(diagnostics, checker, node);
|
||||
}
|
||||
}
|
||||
|
||||
/// UP003
|
||||
pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
|
||||
pub(crate) fn type_of_primitive(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
ast::ExprCall {
|
||||
func, args, range, ..
|
||||
}: &ast::ExprCall,
|
||||
) {
|
||||
if args.len() != 1 {
|
||||
return;
|
||||
}
|
||||
@@ -44,13 +61,13 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr,
|
||||
let Some(primitive) = Primitive::from_constant(value) else {
|
||||
return;
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, expr.range());
|
||||
let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, *range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
primitive.builtin(),
|
||||
expr.range(),
|
||||
*range,
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
|
||||
use crate::autofix::actions::remove_argument;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::Rule;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
@@ -35,6 +36,16 @@ impl AlwaysAutofixableViolation for UnnecessaryEncodeUTF8 {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for UnnecessaryEncodeUTF8 {
|
||||
fn rule() -> Rule {
|
||||
Rule::UnnecessaryEncodeUTF8
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
|
||||
unnecessary_encode_utf8(diagnostics, checker, node);
|
||||
}
|
||||
}
|
||||
|
||||
const UTF8_LITERALS: &[&str] = &["utf-8", "utf8", "utf_8", "u8", "utf", "cp65001"];
|
||||
|
||||
fn match_encoded_variable(func: &Expr) -> Option<&Expr> {
|
||||
@@ -75,8 +86,8 @@ enum EncodingArg<'a> {
|
||||
|
||||
/// Return the encoding argument to an `encode` call, if it can be determined to be a
|
||||
/// UTF-8-equivalent encoding.
|
||||
fn match_encoding_arg<'a>(args: &'a [Expr], kwargs: &'a [Keyword]) -> Option<EncodingArg<'a>> {
|
||||
match (args.len(), kwargs.len()) {
|
||||
fn match_encoding_arg<'a>(args: &'a [Expr], keywords: &'a [Keyword]) -> Option<EncodingArg<'a>> {
|
||||
match (args.len(), keywords.len()) {
|
||||
// Ex `"".encode()`
|
||||
(0, 0) => return Some(EncodingArg::Empty),
|
||||
// Ex `"".encode(encoding)`
|
||||
@@ -88,7 +99,7 @@ fn match_encoding_arg<'a>(args: &'a [Expr], kwargs: &'a [Keyword]) -> Option<Enc
|
||||
}
|
||||
// Ex `"".encode(kwarg=kwarg)`
|
||||
(0, 1) => {
|
||||
let kwarg = &kwargs[0];
|
||||
let kwarg = &keywords[0];
|
||||
if kwarg.arg.as_ref().map_or(false, |arg| arg == "encoding") {
|
||||
if is_utf8_encoding_arg(&kwarg.value) {
|
||||
return Some(EncodingArg::Keyword(kwarg));
|
||||
@@ -102,12 +113,12 @@ fn match_encoding_arg<'a>(args: &'a [Expr], kwargs: &'a [Keyword]) -> Option<Enc
|
||||
}
|
||||
|
||||
/// Return a [`Fix`] replacing the call to encode with a byte string.
|
||||
fn replace_with_bytes_literal(locator: &Locator, expr: &Expr) -> Fix {
|
||||
fn replace_with_bytes_literal(locator: &Locator, range: TextRange) -> Fix {
|
||||
// Build up a replacement string by prefixing all string tokens with `b`.
|
||||
let contents = locator.slice(expr.range());
|
||||
let contents = locator.slice(range);
|
||||
let mut replacement = String::with_capacity(contents.len() + 1);
|
||||
let mut prev = expr.start();
|
||||
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, expr.start()).flatten() {
|
||||
let mut prev = range.start();
|
||||
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, range.start()).flatten() {
|
||||
match tok {
|
||||
Tok::Dot => break,
|
||||
Tok::String { .. } => {
|
||||
@@ -125,16 +136,19 @@ fn replace_with_bytes_literal(locator: &Locator, expr: &Expr) -> Fix {
|
||||
prev = range.end();
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
Fix::unspecified(Edit::range_replacement(replacement, expr.range()))
|
||||
Fix::unspecified(Edit::range_replacement(replacement, range))
|
||||
}
|
||||
|
||||
/// UP012
|
||||
pub(crate) fn unnecessary_encode_utf8(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
kwargs: &[Keyword],
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
range,
|
||||
keywords,
|
||||
}: &ast::ExprCall,
|
||||
) {
|
||||
let Some(variable) = match_encoded_variable(func) else {
|
||||
return;
|
||||
@@ -145,19 +159,19 @@ pub(crate) fn unnecessary_encode_utf8(
|
||||
..
|
||||
}) => {
|
||||
// Ex) `"str".encode()`, `"str".encode("utf-8")`
|
||||
if let Some(encoding_arg) = match_encoding_arg(args, kwargs) {
|
||||
if let Some(encoding_arg) = match_encoding_arg(args, keywords) {
|
||||
if literal.is_ascii() {
|
||||
// Ex) Convert `"foo".encode()` to `b"foo"`.
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryEncodeUTF8 {
|
||||
reason: Reason::BytesLiteral,
|
||||
},
|
||||
expr.range(),
|
||||
*range,
|
||||
);
|
||||
if checker.patch(Rule::UnnecessaryEncodeUTF8) {
|
||||
diagnostic.set_fix(replace_with_bytes_literal(checker.locator, expr));
|
||||
diagnostic.set_fix(replace_with_bytes_literal(checker.locator, *range));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
} else if let EncodingArg::Keyword(kwarg) = encoding_arg {
|
||||
// Ex) Convert `"unicode text©".encode(encoding="utf-8")` to
|
||||
// `"unicode text©".encode()`.
|
||||
@@ -165,7 +179,7 @@ pub(crate) fn unnecessary_encode_utf8(
|
||||
UnnecessaryEncodeUTF8 {
|
||||
reason: Reason::DefaultArgument,
|
||||
},
|
||||
expr.range(),
|
||||
*range,
|
||||
);
|
||||
if checker.patch(Rule::UnnecessaryEncodeUTF8) {
|
||||
#[allow(deprecated)]
|
||||
@@ -175,19 +189,19 @@ pub(crate) fn unnecessary_encode_utf8(
|
||||
func.start(),
|
||||
kwarg.range(),
|
||||
args,
|
||||
kwargs,
|
||||
keywords,
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
} else if let EncodingArg::Positional(arg) = encoding_arg {
|
||||
// Ex) Convert `"unicode text©".encode("utf-8")` to `"unicode text©".encode()`.
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryEncodeUTF8 {
|
||||
reason: Reason::DefaultArgument,
|
||||
},
|
||||
expr.range(),
|
||||
*range,
|
||||
);
|
||||
if checker.patch(Rule::UnnecessaryEncodeUTF8) {
|
||||
#[allow(deprecated)]
|
||||
@@ -197,18 +211,18 @@ pub(crate) fn unnecessary_encode_utf8(
|
||||
func.start(),
|
||||
arg.range(),
|
||||
args,
|
||||
kwargs,
|
||||
keywords,
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ex) `f"foo{bar}".encode("utf-8")`
|
||||
Expr::JoinedStr(_) => {
|
||||
if let Some(encoding_arg) = match_encoding_arg(args, kwargs) {
|
||||
if let Some(encoding_arg) = match_encoding_arg(args, keywords) {
|
||||
if let EncodingArg::Keyword(kwarg) = encoding_arg {
|
||||
// Ex) Convert `f"unicode text©".encode(encoding="utf-8")` to
|
||||
// `f"unicode text©".encode()`.
|
||||
@@ -216,7 +230,7 @@ pub(crate) fn unnecessary_encode_utf8(
|
||||
UnnecessaryEncodeUTF8 {
|
||||
reason: Reason::DefaultArgument,
|
||||
},
|
||||
expr.range(),
|
||||
*range,
|
||||
);
|
||||
if checker.patch(Rule::UnnecessaryEncodeUTF8) {
|
||||
#[allow(deprecated)]
|
||||
@@ -226,19 +240,19 @@ pub(crate) fn unnecessary_encode_utf8(
|
||||
func.start(),
|
||||
kwarg.range(),
|
||||
args,
|
||||
kwargs,
|
||||
keywords,
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
} else if let EncodingArg::Positional(arg) = encoding_arg {
|
||||
// Ex) Convert `f"unicode text©".encode("utf-8")` to `f"unicode text©".encode()`.
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryEncodeUTF8 {
|
||||
reason: Reason::DefaultArgument,
|
||||
},
|
||||
expr.range(),
|
||||
*range,
|
||||
);
|
||||
if checker.patch(Rule::UnnecessaryEncodeUTF8) {
|
||||
#[allow(deprecated)]
|
||||
@@ -248,12 +262,12 @@ pub(crate) fn unnecessary_encode_utf8(
|
||||
func.start(),
|
||||
arg.range(),
|
||||
args,
|
||||
kwargs,
|
||||
keywords,
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ use rustpython_parser::ast::{self, Expr, Operator, Ranged};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::checkers::ast::traits::Analyzer;
|
||||
use crate::checkers::ast::RuleContext;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub(crate) enum CallKind {
|
||||
@@ -50,6 +52,16 @@ impl AlwaysAutofixableViolation for NonPEP604Isinstance {
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer<ast::ExprCall> for NonPEP604Isinstance {
|
||||
fn rule() -> Rule {
|
||||
Rule::NonPEP604Isinstance
|
||||
}
|
||||
|
||||
fn run(diagnostics: &mut Vec<Diagnostic>, checker: &RuleContext, node: &ast::ExprCall) {
|
||||
use_pep604_isinstance(diagnostics, checker, node);
|
||||
}
|
||||
}
|
||||
|
||||
fn union(elts: &[Expr]) -> Expr {
|
||||
if elts.len() == 1 {
|
||||
elts[0].clone()
|
||||
@@ -65,12 +77,17 @@ fn union(elts: &[Expr]) -> Expr {
|
||||
|
||||
/// UP038
|
||||
pub(crate) fn use_pep604_isinstance(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
checker: &RuleContext,
|
||||
ast::ExprCall {
|
||||
func, args, range, ..
|
||||
}: &ast::ExprCall,
|
||||
) {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = func {
|
||||
if checker.settings.target_version < PythonVersion::Py310 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() {
|
||||
let Some(kind) = CallKind::from_name(id) else {
|
||||
return;
|
||||
};
|
||||
@@ -89,7 +106,7 @@ pub(crate) fn use_pep604_isinstance(
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(NonPEP604Isinstance { kind }, expr.range());
|
||||
let mut diagnostic = Diagnostic::new(NonPEP604Isinstance { kind }, *range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
@@ -97,7 +114,7 @@ pub(crate) fn use_pep604_isinstance(
|
||||
types.range(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP032_2.py:2:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
2 | # Errors
|
||||
3 | "{.real}".format(1)
|
||||
| ^^^^^^^^^^^^^^^^^^^ UP032
|
||||
4 | "{0.real}".format(1)
|
||||
5 | "{a.real}".format(a=1)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | # Errors
|
||||
2 |-"{.real}".format(1)
|
||||
2 |+f"{(1).real}"
|
||||
3 3 | "{0.real}".format(1)
|
||||
4 4 | "{a.real}".format(a=1)
|
||||
5 5 |
|
||||
|
||||
UP032_2.py:3:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
3 | # Errors
|
||||
4 | "{.real}".format(1)
|
||||
5 | "{0.real}".format(1)
|
||||
| ^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
6 | "{a.real}".format(a=1)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | # Errors
|
||||
2 2 | "{.real}".format(1)
|
||||
3 |-"{0.real}".format(1)
|
||||
3 |+f"{(1).real}"
|
||||
4 4 | "{a.real}".format(a=1)
|
||||
5 5 |
|
||||
6 6 | "{.real}".format(1.0)
|
||||
|
||||
UP032_2.py:4:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
4 | "{.real}".format(1)
|
||||
5 | "{0.real}".format(1)
|
||||
6 | "{a.real}".format(a=1)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
7 |
|
||||
8 | "{.real}".format(1.0)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | # Errors
|
||||
2 2 | "{.real}".format(1)
|
||||
3 3 | "{0.real}".format(1)
|
||||
4 |-"{a.real}".format(a=1)
|
||||
4 |+f"{(1).real}"
|
||||
5 5 |
|
||||
6 6 | "{.real}".format(1.0)
|
||||
7 7 | "{0.real}".format(1.0)
|
||||
|
||||
UP032_2.py:6:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
6 | "{a.real}".format(a=1)
|
||||
7 |
|
||||
8 | "{.real}".format(1.0)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
9 | "{0.real}".format(1.0)
|
||||
10 | "{a.real}".format(a=1.0)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
3 3 | "{0.real}".format(1)
|
||||
4 4 | "{a.real}".format(a=1)
|
||||
5 5 |
|
||||
6 |-"{.real}".format(1.0)
|
||||
6 |+f"{1.0.real}"
|
||||
7 7 | "{0.real}".format(1.0)
|
||||
8 8 | "{a.real}".format(a=1.0)
|
||||
9 9 |
|
||||
|
||||
UP032_2.py:7:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
7 | "{.real}".format(1.0)
|
||||
8 | "{0.real}".format(1.0)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
9 | "{a.real}".format(a=1.0)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
4 4 | "{a.real}".format(a=1)
|
||||
5 5 |
|
||||
6 6 | "{.real}".format(1.0)
|
||||
7 |-"{0.real}".format(1.0)
|
||||
7 |+f"{1.0.real}"
|
||||
8 8 | "{a.real}".format(a=1.0)
|
||||
9 9 |
|
||||
10 10 | "{.real}".format(1j)
|
||||
|
||||
UP032_2.py:8:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
8 | "{.real}".format(1.0)
|
||||
9 | "{0.real}".format(1.0)
|
||||
10 | "{a.real}".format(a=1.0)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
11 |
|
||||
12 | "{.real}".format(1j)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
5 5 |
|
||||
6 6 | "{.real}".format(1.0)
|
||||
7 7 | "{0.real}".format(1.0)
|
||||
8 |-"{a.real}".format(a=1.0)
|
||||
8 |+f"{1.0.real}"
|
||||
9 9 |
|
||||
10 10 | "{.real}".format(1j)
|
||||
11 11 | "{0.real}".format(1j)
|
||||
|
||||
UP032_2.py:10:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
10 | "{a.real}".format(a=1.0)
|
||||
11 |
|
||||
12 | "{.real}".format(1j)
|
||||
| ^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
13 | "{0.real}".format(1j)
|
||||
14 | "{a.real}".format(a=1j)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
7 7 | "{0.real}".format(1.0)
|
||||
8 8 | "{a.real}".format(a=1.0)
|
||||
9 9 |
|
||||
10 |-"{.real}".format(1j)
|
||||
10 |+f"{1j.real}"
|
||||
11 11 | "{0.real}".format(1j)
|
||||
12 12 | "{a.real}".format(a=1j)
|
||||
13 13 |
|
||||
|
||||
UP032_2.py:11:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
11 | "{.real}".format(1j)
|
||||
12 | "{0.real}".format(1j)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
13 | "{a.real}".format(a=1j)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
8 8 | "{a.real}".format(a=1.0)
|
||||
9 9 |
|
||||
10 10 | "{.real}".format(1j)
|
||||
11 |-"{0.real}".format(1j)
|
||||
11 |+f"{1j.real}"
|
||||
12 12 | "{a.real}".format(a=1j)
|
||||
13 13 |
|
||||
14 14 | "{.real}".format(0b01)
|
||||
|
||||
UP032_2.py:12:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
12 | "{.real}".format(1j)
|
||||
13 | "{0.real}".format(1j)
|
||||
14 | "{a.real}".format(a=1j)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
15 |
|
||||
16 | "{.real}".format(0b01)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
9 9 |
|
||||
10 10 | "{.real}".format(1j)
|
||||
11 11 | "{0.real}".format(1j)
|
||||
12 |-"{a.real}".format(a=1j)
|
||||
12 |+f"{1j.real}"
|
||||
13 13 |
|
||||
14 14 | "{.real}".format(0b01)
|
||||
15 15 | "{0.real}".format(0b01)
|
||||
|
||||
UP032_2.py:14:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
14 | "{a.real}".format(a=1j)
|
||||
15 |
|
||||
16 | "{.real}".format(0b01)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
17 | "{0.real}".format(0b01)
|
||||
18 | "{a.real}".format(a=0b01)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
11 11 | "{0.real}".format(1j)
|
||||
12 12 | "{a.real}".format(a=1j)
|
||||
13 13 |
|
||||
14 |-"{.real}".format(0b01)
|
||||
14 |+f"{0b01.real}"
|
||||
15 15 | "{0.real}".format(0b01)
|
||||
16 16 | "{a.real}".format(a=0b01)
|
||||
17 17 |
|
||||
|
||||
UP032_2.py:15:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
15 | "{.real}".format(0b01)
|
||||
16 | "{0.real}".format(0b01)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
17 | "{a.real}".format(a=0b01)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
12 12 | "{a.real}".format(a=1j)
|
||||
13 13 |
|
||||
14 14 | "{.real}".format(0b01)
|
||||
15 |-"{0.real}".format(0b01)
|
||||
15 |+f"{0b01.real}"
|
||||
16 16 | "{a.real}".format(a=0b01)
|
||||
17 17 |
|
||||
18 18 | "{}".format(1 + 2)
|
||||
|
||||
UP032_2.py:16:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
16 | "{.real}".format(0b01)
|
||||
17 | "{0.real}".format(0b01)
|
||||
18 | "{a.real}".format(a=0b01)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
19 |
|
||||
20 | "{}".format(1 + 2)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
13 13 |
|
||||
14 14 | "{.real}".format(0b01)
|
||||
15 15 | "{0.real}".format(0b01)
|
||||
16 |-"{a.real}".format(a=0b01)
|
||||
16 |+f"{0b01.real}"
|
||||
17 17 |
|
||||
18 18 | "{}".format(1 + 2)
|
||||
19 19 | "{}".format([1, 2])
|
||||
|
||||
UP032_2.py:18:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
18 | "{a.real}".format(a=0b01)
|
||||
19 |
|
||||
20 | "{}".format(1 + 2)
|
||||
| ^^^^^^^^^^^^^^^^^^ UP032
|
||||
21 | "{}".format([1, 2])
|
||||
22 | "{}".format({1, 2})
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
15 15 | "{0.real}".format(0b01)
|
||||
16 16 | "{a.real}".format(a=0b01)
|
||||
17 17 |
|
||||
18 |-"{}".format(1 + 2)
|
||||
18 |+f"{1 + 2}"
|
||||
19 19 | "{}".format([1, 2])
|
||||
20 20 | "{}".format({1, 2})
|
||||
21 21 | "{}".format({1: 2, 3: 4})
|
||||
|
||||
UP032_2.py:19:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
19 | "{}".format(1 + 2)
|
||||
20 | "{}".format([1, 2])
|
||||
| ^^^^^^^^^^^^^^^^^^^ UP032
|
||||
21 | "{}".format({1, 2})
|
||||
22 | "{}".format({1: 2, 3: 4})
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
16 16 | "{a.real}".format(a=0b01)
|
||||
17 17 |
|
||||
18 18 | "{}".format(1 + 2)
|
||||
19 |-"{}".format([1, 2])
|
||||
19 |+f"{[1, 2]}"
|
||||
20 20 | "{}".format({1, 2})
|
||||
21 21 | "{}".format({1: 2, 3: 4})
|
||||
22 22 | "{}".format((i for i in range(2)))
|
||||
|
||||
UP032_2.py:20:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
20 | "{}".format(1 + 2)
|
||||
21 | "{}".format([1, 2])
|
||||
22 | "{}".format({1, 2})
|
||||
| ^^^^^^^^^^^^^^^^^^^ UP032
|
||||
23 | "{}".format({1: 2, 3: 4})
|
||||
24 | "{}".format((i for i in range(2)))
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
17 17 |
|
||||
18 18 | "{}".format(1 + 2)
|
||||
19 19 | "{}".format([1, 2])
|
||||
20 |-"{}".format({1, 2})
|
||||
20 |+f"{({1, 2})}"
|
||||
21 21 | "{}".format({1: 2, 3: 4})
|
||||
22 22 | "{}".format((i for i in range(2)))
|
||||
23 23 |
|
||||
|
||||
UP032_2.py:21:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
21 | "{}".format([1, 2])
|
||||
22 | "{}".format({1, 2})
|
||||
23 | "{}".format({1: 2, 3: 4})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
24 | "{}".format((i for i in range(2)))
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
18 18 | "{}".format(1 + 2)
|
||||
19 19 | "{}".format([1, 2])
|
||||
20 20 | "{}".format({1, 2})
|
||||
21 |-"{}".format({1: 2, 3: 4})
|
||||
21 |+f"{({1: 2, 3: 4})}"
|
||||
22 22 | "{}".format((i for i in range(2)))
|
||||
23 23 |
|
||||
24 24 | "{.real}".format(1 + 2)
|
||||
|
||||
UP032_2.py:22:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
22 | "{}".format({1, 2})
|
||||
23 | "{}".format({1: 2, 3: 4})
|
||||
24 | "{}".format((i for i in range(2)))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
25 |
|
||||
26 | "{.real}".format(1 + 2)
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
19 19 | "{}".format([1, 2])
|
||||
20 20 | "{}".format({1, 2})
|
||||
21 21 | "{}".format({1: 2, 3: 4})
|
||||
22 |-"{}".format((i for i in range(2)))
|
||||
22 |+f"{(i for i in range(2))}"
|
||||
23 23 |
|
||||
24 24 | "{.real}".format(1 + 2)
|
||||
25 25 | "{.real}".format([1, 2])
|
||||
|
||||
UP032_2.py:24:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
24 | "{}".format((i for i in range(2)))
|
||||
25 |
|
||||
26 | "{.real}".format(1 + 2)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
27 | "{.real}".format([1, 2])
|
||||
28 | "{.real}".format({1, 2})
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
21 21 | "{}".format({1: 2, 3: 4})
|
||||
22 22 | "{}".format((i for i in range(2)))
|
||||
23 23 |
|
||||
24 |-"{.real}".format(1 + 2)
|
||||
24 |+f"{(1 + 2).real}"
|
||||
25 25 | "{.real}".format([1, 2])
|
||||
26 26 | "{.real}".format({1, 2})
|
||||
27 27 | "{.real}".format({1: 2, 3: 4})
|
||||
|
||||
UP032_2.py:25:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
25 | "{.real}".format(1 + 2)
|
||||
26 | "{.real}".format([1, 2])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
27 | "{.real}".format({1, 2})
|
||||
28 | "{.real}".format({1: 2, 3: 4})
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
22 22 | "{}".format((i for i in range(2)))
|
||||
23 23 |
|
||||
24 24 | "{.real}".format(1 + 2)
|
||||
25 |-"{.real}".format([1, 2])
|
||||
25 |+f"{[1, 2].real}"
|
||||
26 26 | "{.real}".format({1, 2})
|
||||
27 27 | "{.real}".format({1: 2, 3: 4})
|
||||
28 28 | "{}".format((i for i in range(2)))
|
||||
|
||||
UP032_2.py:26:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
26 | "{.real}".format(1 + 2)
|
||||
27 | "{.real}".format([1, 2])
|
||||
28 | "{.real}".format({1, 2})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
29 | "{.real}".format({1: 2, 3: 4})
|
||||
30 | "{}".format((i for i in range(2)))
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
23 23 |
|
||||
24 24 | "{.real}".format(1 + 2)
|
||||
25 25 | "{.real}".format([1, 2])
|
||||
26 |-"{.real}".format({1, 2})
|
||||
26 |+f"{({1, 2}).real}"
|
||||
27 27 | "{.real}".format({1: 2, 3: 4})
|
||||
28 28 | "{}".format((i for i in range(2)))
|
||||
|
||||
UP032_2.py:27:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
27 | "{.real}".format([1, 2])
|
||||
28 | "{.real}".format({1, 2})
|
||||
29 | "{.real}".format({1: 2, 3: 4})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
30 | "{}".format((i for i in range(2)))
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
24 24 | "{.real}".format(1 + 2)
|
||||
25 25 | "{.real}".format([1, 2])
|
||||
26 26 | "{.real}".format({1, 2})
|
||||
27 |-"{.real}".format({1: 2, 3: 4})
|
||||
27 |+f"{({1: 2, 3: 4}).real}"
|
||||
28 28 | "{}".format((i for i in range(2)))
|
||||
|
||||
UP032_2.py:28:1: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
28 | "{.real}".format({1, 2})
|
||||
29 | "{.real}".format({1: 2, 3: 4})
|
||||
30 | "{}".format((i for i in range(2)))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Suggested fix
|
||||
25 25 | "{.real}".format([1, 2])
|
||||
26 26 | "{.real}".format({1, 2})
|
||||
27 27 | "{.real}".format({1: 2, 3: 4})
|
||||
28 |-"{}".format((i for i in range(2)))
|
||||
28 |+f"{(i for i in range(2))}"
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use rustpython_parser::ast::{self, ConversionFlag, Expr, Ranged};
|
||||
use anyhow::{bail, Result};
|
||||
use libcst_native::{Codegen, CodegenState};
|
||||
use rustpython_parser::ast::{self, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::cst::matchers::{
|
||||
match_call, match_expression, match_formatted_string, match_formatted_string_expression,
|
||||
match_name,
|
||||
};
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
@@ -39,59 +46,101 @@ impl AlwaysAutofixableViolation for ExplicitFStringTypeConversion {
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_explicit_f_string_type_conversion(
|
||||
expr: &Expr,
|
||||
index: usize,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Fix> {
|
||||
// Replace the call node with its argument and a conversion flag.
|
||||
let range = expr.range();
|
||||
let content = locator.slice(range);
|
||||
let mut expression = match_expression(content)?;
|
||||
let formatted_string = match_formatted_string(&mut expression)?;
|
||||
|
||||
// Replace the formatted call expression at `index` with a conversion flag.
|
||||
let mut formatted_string_expression =
|
||||
match_formatted_string_expression(&mut formatted_string.parts[index])?;
|
||||
let call = match_call(&mut formatted_string_expression.expression)?;
|
||||
let name = match_name(&mut call.func)?;
|
||||
match name.value {
|
||||
"str" => {
|
||||
formatted_string_expression.conversion = Some("s");
|
||||
}
|
||||
"repr" => {
|
||||
formatted_string_expression.conversion = Some("r");
|
||||
}
|
||||
"ascii" => {
|
||||
formatted_string_expression.conversion = Some("a");
|
||||
}
|
||||
_ => bail!("Unexpected function call: `{:?}`", name.value),
|
||||
}
|
||||
formatted_string_expression.expression = call.args[0].value.clone();
|
||||
|
||||
let mut state = CodegenState {
|
||||
default_newline: &stylist.line_ending(),
|
||||
default_indent: stylist.indentation(),
|
||||
..CodegenState::default()
|
||||
};
|
||||
expression.codegen(&mut state);
|
||||
|
||||
Ok(Fix::automatic(Edit::range_replacement(
|
||||
state.to_string(),
|
||||
range,
|
||||
)))
|
||||
}
|
||||
|
||||
/// RUF010
|
||||
pub(crate) fn explicit_f_string_type_conversion(
|
||||
checker: &mut Checker,
|
||||
formatted_value: &Expr,
|
||||
conversion: ConversionFlag,
|
||||
expr: &Expr,
|
||||
values: &[Expr],
|
||||
) {
|
||||
// Skip if there's already a conversion flag.
|
||||
if !conversion.is_none() {
|
||||
return;
|
||||
for (index, formatted_value) in values.iter().enumerate() {
|
||||
let Expr::FormattedValue(ast::ExprFormattedValue {
|
||||
conversion,
|
||||
value,
|
||||
..
|
||||
}) = &formatted_value else {
|
||||
continue;
|
||||
};
|
||||
// Skip if there's already a conversion flag.
|
||||
if !conversion.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
..
|
||||
}) = value.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Can't be a conversion otherwise.
|
||||
if args.len() != 1 || !keywords.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !matches!(id.as_str(), "str" | "repr" | "ascii") {
|
||||
return;
|
||||
};
|
||||
|
||||
if !checker.ctx.is_builtin(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, value.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fix_explicit_f_string_type_conversion(expr, index, checker.locator, checker.stylist)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
let Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
range: _,
|
||||
}) = formatted_value else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Can't be a conversion otherwise.
|
||||
if args.len() != 1 || !keywords.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let conversion = match id.as_str() {
|
||||
"ascii" => 'a',
|
||||
"str" => 's',
|
||||
"repr" => 'r',
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if !checker.ctx.is_builtin(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let formatted_value_range = formatted_value.range();
|
||||
let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, formatted_value_range);
|
||||
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let arg_range = args[0].range();
|
||||
let remove_call = Edit::deletion(formatted_value_range.start(), arg_range.start());
|
||||
let add_conversion = Edit::replacement(
|
||||
format!("!{conversion}"),
|
||||
arg_range.end(),
|
||||
formatted_value_range.end(),
|
||||
);
|
||||
diagnostic.set_fix(Fix::automatic_edits(remove_call, [add_conversion]));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ RUF010.py:11:4: RUF010 [*] Use conversion in f-string
|
||||
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
| ^^^^^^^^^^^ RUF010
|
||||
14 |
|
||||
15 | f"{foo(bla)}" # OK
|
||||
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
|
|
||||
= help: Replace f-string function call with conversion
|
||||
|
||||
@@ -76,7 +76,7 @@ RUF010.py:11:4: RUF010 [*] Use conversion in f-string
|
||||
11 |-f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
11 |+f"{d['a']!s}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
12 12 |
|
||||
13 13 | f"{foo(bla)}" # OK
|
||||
13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
14 14 |
|
||||
|
||||
RUF010.py:11:19: RUF010 [*] Use conversion in f-string
|
||||
@@ -86,7 +86,7 @@ RUF010.py:11:19: RUF010 [*] Use conversion in f-string
|
||||
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
| ^^^^^^^^^^^^ RUF010
|
||||
14 |
|
||||
15 | f"{foo(bla)}" # OK
|
||||
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
|
|
||||
= help: Replace f-string function call with conversion
|
||||
|
||||
@@ -97,7 +97,7 @@ RUF010.py:11:19: RUF010 [*] Use conversion in f-string
|
||||
11 |-f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
11 |+f"{str(d['a'])}, {d['b']!r}, {ascii(d['c'])}" # RUF010
|
||||
12 12 |
|
||||
13 13 | f"{foo(bla)}" # OK
|
||||
13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
14 14 |
|
||||
|
||||
RUF010.py:11:35: RUF010 [*] Use conversion in f-string
|
||||
@@ -107,7 +107,7 @@ RUF010.py:11:35: RUF010 [*] Use conversion in f-string
|
||||
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
| ^^^^^^^^^^^^^ RUF010
|
||||
14 |
|
||||
15 | f"{foo(bla)}" # OK
|
||||
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
|
|
||||
= help: Replace f-string function call with conversion
|
||||
|
||||
@@ -118,7 +118,70 @@ RUF010.py:11:35: RUF010 [*] Use conversion in f-string
|
||||
11 |-f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
11 |+f"{str(d['a'])}, {repr(d['b'])}, {d['c']!a}" # RUF010
|
||||
12 12 |
|
||||
13 13 | f"{foo(bla)}" # OK
|
||||
13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
14 14 |
|
||||
|
||||
RUF010.py:13:5: RUF010 [*] Use conversion in f-string
|
||||
|
|
||||
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
14 |
|
||||
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
| ^^^^^^^^ RUF010
|
||||
16 |
|
||||
17 | f"{foo(bla)}" # OK
|
||||
|
|
||||
= help: Replace f-string function call with conversion
|
||||
|
||||
ℹ Fix
|
||||
10 10 |
|
||||
11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
12 12 |
|
||||
13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
13 |+f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
14 14 |
|
||||
15 15 | f"{foo(bla)}" # OK
|
||||
16 16 |
|
||||
|
||||
RUF010.py:13:19: RUF010 [*] Use conversion in f-string
|
||||
|
|
||||
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
14 |
|
||||
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
| ^^^^^^^^^ RUF010
|
||||
16 |
|
||||
17 | f"{foo(bla)}" # OK
|
||||
|
|
||||
= help: Replace f-string function call with conversion
|
||||
|
||||
ℹ Fix
|
||||
10 10 |
|
||||
11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
12 12 |
|
||||
13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
13 |+f"{(str(bla))}, {bla!r}, {(ascii(bla))}" # RUF010
|
||||
14 14 |
|
||||
15 15 | f"{foo(bla)}" # OK
|
||||
16 16 |
|
||||
|
||||
RUF010.py:13:34: RUF010 [*] Use conversion in f-string
|
||||
|
|
||||
13 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
14 |
|
||||
15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
| ^^^^^^^^^^ RUF010
|
||||
16 |
|
||||
17 | f"{foo(bla)}" # OK
|
||||
|
|
||||
= help: Replace f-string function call with conversion
|
||||
|
||||
ℹ Fix
|
||||
10 10 |
|
||||
11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
|
||||
12 12 |
|
||||
13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
|
||||
13 |+f"{(str(bla))}, {(repr(bla))}, {bla!a}" # RUF010
|
||||
14 14 |
|
||||
15 15 | f"{foo(bla)}" # OK
|
||||
16 16 |
|
||||
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ pub(crate) fn useless_try_except(checker: &mut Checker, handlers: &[Excepthandle
|
||||
.iter()
|
||||
.map(|handler| {
|
||||
let ExceptHandler(ExcepthandlerExceptHandler { name, body, .. }) = handler;
|
||||
let Some(Stmt::Raise(ast::StmtRaise { exc, .. })) = &body.first() else {
|
||||
let Some(Stmt::Raise(ast::StmtRaise { exc, cause: None, .. })) = &body.first() else {
|
||||
return None;
|
||||
};
|
||||
if let Some(expr) = exc {
|
||||
|
||||
@@ -32,6 +32,7 @@ pub struct RuleSelection {
|
||||
pub extend_select: Vec<RuleSelector>,
|
||||
pub fixable: Option<Vec<RuleSelector>>,
|
||||
pub unfixable: Vec<RuleSelector>,
|
||||
pub extend_fixable: Vec<RuleSelector>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -47,6 +48,7 @@ pub struct Configuration {
|
||||
pub extend: Option<PathBuf>,
|
||||
pub extend_exclude: Vec<FilePattern>,
|
||||
pub extend_include: Vec<FilePattern>,
|
||||
pub extend_per_file_ignores: Vec<PerFileIgnore>,
|
||||
pub external: Option<Vec<String>>,
|
||||
pub fix: Option<bool>,
|
||||
pub fix_only: Option<bool>,
|
||||
@@ -101,7 +103,13 @@ impl Configuration {
|
||||
.collect(),
|
||||
extend_select: options.extend_select.unwrap_or_default(),
|
||||
fixable: options.fixable,
|
||||
unfixable: options.unfixable.unwrap_or_default(),
|
||||
unfixable: options
|
||||
.unfixable
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(options.extend_unfixable.into_iter().flatten())
|
||||
.collect(),
|
||||
extend_fixable: options.extend_fixable.unwrap_or_default(),
|
||||
}],
|
||||
allowed_confusables: options.allowed_confusables,
|
||||
builtins: options.builtins,
|
||||
@@ -159,6 +167,17 @@ impl Configuration {
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
extend_per_file_ignores: options
|
||||
.extend_per_file_ignores
|
||||
.map(|per_file_ignores| {
|
||||
per_file_ignores
|
||||
.into_iter()
|
||||
.map(|(pattern, prefixes)| {
|
||||
PerFileIgnore::new(pattern, &prefixes, Some(project_root))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
external: options.external,
|
||||
fix: options.fix,
|
||||
fix_only: options.fix_only,
|
||||
@@ -247,6 +266,11 @@ impl Configuration {
|
||||
.into_iter()
|
||||
.chain(self.extend_include.into_iter())
|
||||
.collect(),
|
||||
extend_per_file_ignores: config
|
||||
.extend_per_file_ignores
|
||||
.into_iter()
|
||||
.chain(self.extend_per_file_ignores.into_iter())
|
||||
.collect(),
|
||||
external: self.external.or(config.external),
|
||||
fix: self.fix.or(config.fix),
|
||||
fix_only: self.fix_only.or(config.fix_only),
|
||||
|
||||
@@ -163,7 +163,12 @@ impl Settings {
|
||||
line_length: config.line_length.unwrap_or(defaults::LINE_LENGTH),
|
||||
namespace_packages: config.namespace_packages.unwrap_or_default(),
|
||||
per_file_ignores: resolve_per_file_ignores(
|
||||
config.per_file_ignores.unwrap_or_default(),
|
||||
config
|
||||
.per_file_ignores
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(config.extend_per_file_ignores)
|
||||
.collect(),
|
||||
)?,
|
||||
respect_gitignore: config.respect_gitignore.unwrap_or(true),
|
||||
src: config
|
||||
@@ -258,16 +263,11 @@ impl From<&Configuration> for RuleTable {
|
||||
// across config files (which otherwise wouldn't be possible since ruff
|
||||
// only has `extended` but no `extended-by`).
|
||||
let mut carryover_ignores: Option<&[RuleSelector]> = None;
|
||||
let mut carryover_unfixables: Option<&[RuleSelector]> = None;
|
||||
|
||||
let mut redirects = FxHashMap::default();
|
||||
|
||||
for selection in &config.rule_selections {
|
||||
// We do not have an extend-fixable option, so fixable and unfixable
|
||||
// selectors can simply be applied directly to fixable_set.
|
||||
if selection.fixable.is_some() {
|
||||
fixable_set.clear();
|
||||
}
|
||||
|
||||
// If a selection only specifies extend-select we cannot directly
|
||||
// apply its rule selectors to the select_set because we firstly have
|
||||
// to resolve the effectively selected rules within the current rule selection
|
||||
@@ -276,10 +276,13 @@ impl From<&Configuration> for RuleTable {
|
||||
// We do this via the following HashMap where the bool indicates
|
||||
// whether to enable or disable the given rule.
|
||||
let mut select_map_updates: FxHashMap<Rule, bool> = FxHashMap::default();
|
||||
let mut fixable_map_updates: FxHashMap<Rule, bool> = FxHashMap::default();
|
||||
|
||||
let carriedover_ignores = carryover_ignores.take();
|
||||
let carriedover_unfixables = carryover_unfixables.take();
|
||||
|
||||
for spec in Specificity::iter() {
|
||||
// Iterate over rule selectors in order of specificity.
|
||||
for selector in selection
|
||||
.select
|
||||
.iter()
|
||||
@@ -301,17 +304,26 @@ impl From<&Configuration> for RuleTable {
|
||||
select_map_updates.insert(rule, false);
|
||||
}
|
||||
}
|
||||
if let Some(fixable) = &selection.fixable {
|
||||
fixable_set
|
||||
.extend(fixable.iter().filter(|s| s.specificity() == spec).flatten());
|
||||
// Apply the same logic to `fixable` and `unfixable`.
|
||||
for selector in selection
|
||||
.fixable
|
||||
.iter()
|
||||
.flatten()
|
||||
.chain(selection.extend_fixable.iter())
|
||||
.filter(|s| s.specificity() == spec)
|
||||
{
|
||||
for rule in selector {
|
||||
fixable_map_updates.insert(rule, true);
|
||||
}
|
||||
}
|
||||
for selector in selection
|
||||
.unfixable
|
||||
.iter()
|
||||
.chain(carriedover_unfixables.into_iter().flatten())
|
||||
.filter(|s| s.specificity() == spec)
|
||||
{
|
||||
for rule in selector {
|
||||
fixable_set.remove(rule);
|
||||
fixable_map_updates.insert(rule, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,6 +353,29 @@ impl From<&Configuration> for RuleTable {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the same logic to `fixable` and `unfixable`.
|
||||
if let Some(fixable) = &selection.fixable {
|
||||
fixable_set = fixable_map_updates
|
||||
.into_iter()
|
||||
.filter_map(|(rule, enabled)| enabled.then_some(rule))
|
||||
.collect();
|
||||
|
||||
if fixable.is_empty()
|
||||
&& selection.extend_fixable.is_empty()
|
||||
&& !selection.unfixable.is_empty()
|
||||
{
|
||||
carryover_unfixables = Some(&selection.unfixable);
|
||||
}
|
||||
} else {
|
||||
for (rule, enabled) in fixable_map_updates {
|
||||
if enabled {
|
||||
fixable_set.insert(rule);
|
||||
} else {
|
||||
fixable_set.remove(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We insert redirects into the hashmap so that we
|
||||
// can warn the users about remapped rule codes.
|
||||
for selector in selection
|
||||
@@ -351,6 +386,7 @@ impl From<&Configuration> for RuleTable {
|
||||
.chain(selection.ignore.iter())
|
||||
.chain(selection.extend_select.iter())
|
||||
.chain(selection.unfixable.iter())
|
||||
.chain(selection.extend_fixable.iter())
|
||||
{
|
||||
if let RuleSelector::Prefix {
|
||||
prefix,
|
||||
|
||||
@@ -175,6 +175,24 @@ pub struct Options {
|
||||
/// A list of rule codes or prefixes to enable, in addition to those
|
||||
/// specified by `select`.
|
||||
pub extend_select: Option<Vec<RuleSelector>>,
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
value_type = "list[RuleSelector]",
|
||||
example = r#"
|
||||
# Enable autofix for flake8-bugbear (`B`), on top of any rules specified by `fixable`.
|
||||
extend-fixable = ["B"]
|
||||
"#
|
||||
)]
|
||||
/// A list of rule codes or prefixes to consider autofixable, in addition to those
|
||||
/// specified by `fixable`.
|
||||
pub extend_fixable: Option<Vec<RuleSelector>>,
|
||||
/// A list of rule codes or prefixes to consider non-auto-fixable, in addition to those
|
||||
/// specified by `unfixable`.
|
||||
///
|
||||
/// This option has been **deprecated** in favor of `unfixable` since its usage is now
|
||||
/// interchangeable with `unfixable`.
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
pub extend_unfixable: Option<Vec<RuleSelector>>,
|
||||
#[option(
|
||||
default = "[]",
|
||||
value_type = "list[str]",
|
||||
@@ -523,4 +541,16 @@ pub struct Options {
|
||||
/// A list of mappings from file pattern to rule codes or prefixes to
|
||||
/// exclude, when considering any matching files.
|
||||
pub per_file_ignores: Option<FxHashMap<String, Vec<RuleSelector>>>,
|
||||
#[option(
|
||||
default = "{}",
|
||||
value_type = "dict[str, list[RuleSelector]]",
|
||||
example = r#"
|
||||
# Also ignore `E401` in all `__init__.py` files.
|
||||
[tool.ruff.extend-per-file-ignores]
|
||||
"__init__.py" = ["E402"]
|
||||
"#
|
||||
)]
|
||||
/// A list of mappings from file pattern to rule codes or prefixes to
|
||||
/// exclude, in addition to any rules excluded by `per-file-ignores`.
|
||||
pub extend_per_file_ignores: Option<FxHashMap<String, Vec<RuleSelector>>>,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.267"
|
||||
version = "0.0.269"
|
||||
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
|
||||
@@ -57,6 +57,13 @@ pub enum Command {
|
||||
/// Generate shell completion.
|
||||
#[clap(alias = "--generate-shell-completion", hide = true)]
|
||||
GenerateShellCompletion { shell: clap_complete_command::Shell },
|
||||
/// Format the given files, or stdin when using `-`.
|
||||
#[doc(hidden)]
|
||||
#[clap(hide = true)]
|
||||
Format {
|
||||
/// List of files or directories to format or `-` for stdin
|
||||
files: Vec<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Args)]
|
||||
@@ -126,8 +133,8 @@ pub struct CheckArgs {
|
||||
hide_possible_values = true
|
||||
)]
|
||||
pub ignore: Option<Vec<RuleSelector>>,
|
||||
/// Like --select, but adds additional rule codes on top of the selected
|
||||
/// ones.
|
||||
/// Like --select, but adds additional rule codes on top of those already
|
||||
/// specified.
|
||||
#[arg(
|
||||
long,
|
||||
value_delimiter = ',',
|
||||
@@ -147,9 +154,13 @@ pub struct CheckArgs {
|
||||
hide = true
|
||||
)]
|
||||
pub extend_ignore: Option<Vec<RuleSelector>>,
|
||||
/// List of mappings from file pattern to code to exclude
|
||||
/// List of mappings from file pattern to code to exclude.
|
||||
#[arg(long, value_delimiter = ',', help_heading = "Rule selection")]
|
||||
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
|
||||
/// Like `--per-file-ignores`, but adds additional ignores on top of
|
||||
/// those already specified.
|
||||
#[arg(long, value_delimiter = ',', help_heading = "Rule selection")]
|
||||
pub extend_per_file_ignores: Option<Vec<PatternPrefixPair>>,
|
||||
/// List of paths, used to omit files and/or directories from analysis.
|
||||
#[arg(
|
||||
long,
|
||||
@@ -189,6 +200,27 @@ pub struct CheckArgs {
|
||||
hide_possible_values = true
|
||||
)]
|
||||
pub unfixable: Option<Vec<RuleSelector>>,
|
||||
/// Like --fixable, but adds additional rule codes on top of those already
|
||||
/// specified.
|
||||
#[arg(
|
||||
long,
|
||||
value_delimiter = ',',
|
||||
value_name = "RULE_CODE",
|
||||
value_parser = parse_rule_selector,
|
||||
help_heading = "Rule selection",
|
||||
hide_possible_values = true
|
||||
)]
|
||||
pub extend_fixable: Option<Vec<RuleSelector>>,
|
||||
/// Like --unfixable. (Deprecated: You can just use --unfixable instead.)
|
||||
#[arg(
|
||||
long,
|
||||
value_delimiter = ',',
|
||||
value_name = "RULE_CODE",
|
||||
value_parser = parse_rule_selector,
|
||||
help_heading = "Rule selection",
|
||||
hide = true
|
||||
)]
|
||||
pub extend_unfixable: Option<Vec<RuleSelector>>,
|
||||
/// Respect file exclusions via `.gitignore` and other standard ignore
|
||||
/// files.
|
||||
#[arg(
|
||||
@@ -375,8 +407,10 @@ impl CheckArgs {
|
||||
dummy_variable_rgx: self.dummy_variable_rgx,
|
||||
exclude: self.exclude,
|
||||
extend_exclude: self.extend_exclude,
|
||||
extend_fixable: self.extend_fixable,
|
||||
extend_ignore: self.extend_ignore,
|
||||
extend_select: self.extend_select,
|
||||
extend_unfixable: self.extend_unfixable,
|
||||
fixable: self.fixable,
|
||||
ignore: self.ignore,
|
||||
line_length: self.line_length,
|
||||
@@ -442,8 +476,10 @@ pub struct Overrides {
|
||||
pub dummy_variable_rgx: Option<Regex>,
|
||||
pub exclude: Option<Vec<FilePattern>>,
|
||||
pub extend_exclude: Option<Vec<FilePattern>>,
|
||||
pub extend_fixable: Option<Vec<RuleSelector>>,
|
||||
pub extend_ignore: Option<Vec<RuleSelector>>,
|
||||
pub extend_select: Option<Vec<RuleSelector>>,
|
||||
pub extend_unfixable: Option<Vec<RuleSelector>>,
|
||||
pub fixable: Option<Vec<RuleSelector>>,
|
||||
pub ignore: Option<Vec<RuleSelector>>,
|
||||
pub line_length: Option<usize>,
|
||||
@@ -493,7 +529,14 @@ impl ConfigProcessor for &Overrides {
|
||||
.collect(),
|
||||
extend_select: self.extend_select.clone().unwrap_or_default(),
|
||||
fixable: self.fixable.clone(),
|
||||
unfixable: self.unfixable.clone().unwrap_or_default(),
|
||||
unfixable: self
|
||||
.unfixable
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(self.extend_unfixable.iter().cloned())
|
||||
.flatten()
|
||||
.collect(),
|
||||
extend_fixable: self.extend_fixable.clone().unwrap_or_default(),
|
||||
});
|
||||
if let Some(format) = &self.format {
|
||||
config.format = Some(*format);
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::args::Overrides;
|
||||
use crate::diagnostics::{lint_stdin, Diagnostics};
|
||||
|
||||
/// Read a `String` from `stdin`.
|
||||
fn read_from_stdin() -> Result<String> {
|
||||
pub(crate) fn read_from_stdin() -> Result<String> {
|
||||
let mut buffer = String::new();
|
||||
io::stdin().lock().read_to_string(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::io::{self, BufWriter};
|
||||
use std::path::PathBuf;
|
||||
use std::io::{self, stdout, BufWriter, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use clap::CommandFactory;
|
||||
use log::warn;
|
||||
use notify::{recommended_watcher, RecursiveMode, Watcher};
|
||||
|
||||
use ruff::logging::{set_up_logging, LogLevel};
|
||||
@@ -13,6 +14,7 @@ use ruff::settings::{flags, CliSettings};
|
||||
use ruff::{fs, warn_user_once};
|
||||
|
||||
use crate::args::{Args, CheckArgs, Command};
|
||||
use crate::commands::run_stdin::read_from_stdin;
|
||||
use crate::printer::{Flags as PrinterFlags, Printer};
|
||||
|
||||
pub mod args;
|
||||
@@ -117,11 +119,41 @@ quoting the executed command, along with the relevant file contents and `pyproje
|
||||
shell.generate(&mut Args::command(), &mut io::stdout());
|
||||
}
|
||||
Command::Check(args) => return check(args, log_level),
|
||||
Command::Format { files } => return format(&files),
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn format(files: &[PathBuf]) -> Result<ExitStatus> {
|
||||
warn_user_once!(
|
||||
"`ruff format` is a work-in-progress, subject to change at any time, and intended for \
|
||||
internal use only."
|
||||
);
|
||||
|
||||
// dummy
|
||||
let format_code = |code: &str| code.replace("# DEL", "");
|
||||
|
||||
match &files {
|
||||
// Check if we should read from stdin
|
||||
[path] if path == Path::new("-") => {
|
||||
let unformatted = read_from_stdin()?;
|
||||
let formatted = format_code(&unformatted);
|
||||
stdout().lock().write_all(formatted.as_bytes())?;
|
||||
}
|
||||
_ => {
|
||||
for file in files {
|
||||
let unformatted = std::fs::read_to_string(file)
|
||||
.with_context(|| format!("Could not read {}: ", file.display()))?;
|
||||
let formatted = format_code(&unformatted);
|
||||
std::fs::write(file, formatted)
|
||||
.with_context(|| format!("Could not write to {}, exiting", file.display()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn check(args: CheckArgs, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
#[cfg(feature = "ecosystem_ci")]
|
||||
let ecosystem_ci = args.ecosystem_ci;
|
||||
|
||||
@@ -19,7 +19,6 @@ memchr = "2.5.0"
|
||||
num-bigint = { version = "0.4.3" }
|
||||
num-traits = { version = "0.2.15" }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
rustpython-literal = { workspace = true }
|
||||
rustpython-parser = { workspace = true }
|
||||
|
||||
@@ -4,8 +4,6 @@ use std::path::Path;
|
||||
use itertools::Itertools;
|
||||
use log::error;
|
||||
use num_traits::Zero;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustpython_parser::ast::{
|
||||
@@ -542,7 +540,9 @@ where
|
||||
body.iter().any(|stmt| any_over_stmt(stmt, func))
|
||||
}
|
||||
|
||||
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
|
||||
fn is_dunder(id: &str) -> bool {
|
||||
id.starts_with("__") && id.ends_with("__")
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Stmt`] is an assignment to a dunder (like `__all__`).
|
||||
pub fn is_assignment_to_a_dunder(stmt: &Stmt) -> bool {
|
||||
@@ -553,15 +553,19 @@ pub fn is_assignment_to_a_dunder(stmt: &Stmt) -> bool {
|
||||
if targets.len() != 1 {
|
||||
return false;
|
||||
}
|
||||
match &targets[0] {
|
||||
Expr::Name(ast::ExprName { id, .. }) => DUNDER_REGEX.is_match(id.as_str()),
|
||||
_ => false,
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = &targets[0] {
|
||||
is_dunder(id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
|
||||
is_dunder(id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => match target.as_ref() {
|
||||
Expr::Name(ast::ExprName { id, .. }) => DUNDER_REGEX.is_match(id.as_str()),
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::Path;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use nohash_hasher::{BuildNoHashHasher, IntMap};
|
||||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{Expr, Stmt};
|
||||
use smallvec::smallvec;
|
||||
|
||||
@@ -113,6 +114,141 @@ impl<'a> Context<'a> {
|
||||
.map_or(false, |binding| binding.kind.is_builtin())
|
||||
}
|
||||
|
||||
/// Resolve a reference to the given symbol.
|
||||
pub fn resolve_reference(&mut self, symbol: &str, range: TextRange) -> ResolvedReference {
|
||||
// PEP 563 indicates that if a forward reference can be resolved in the module scope, we
|
||||
// should prefer it over local resolutions.
|
||||
if self.in_deferred_type_definition() {
|
||||
if let Some(binding_id) = self.scopes.global().get(symbol) {
|
||||
// Mark the binding as used.
|
||||
let context = self.execution_context();
|
||||
self.bindings[*binding_id].mark_used(ScopeId::global(), range, context);
|
||||
|
||||
// Mark any submodule aliases as used.
|
||||
if let Some(binding_id) = self.resolve_submodule(ScopeId::global(), *binding_id) {
|
||||
self.bindings[binding_id].mark_used(ScopeId::global(), range, context);
|
||||
}
|
||||
|
||||
return ResolvedReference::Resolved(*binding_id);
|
||||
}
|
||||
}
|
||||
|
||||
let mut seen_function = false;
|
||||
let mut import_starred = false;
|
||||
for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() {
|
||||
let scope = &self.scopes[scope_id];
|
||||
if scope.kind.is_class() {
|
||||
// Allow usages of `__class__` within methods, e.g.:
|
||||
//
|
||||
// ```python
|
||||
// class Foo:
|
||||
// def __init__(self):
|
||||
// print(__class__)
|
||||
// ```
|
||||
if seen_function && matches!(symbol, "__class__") {
|
||||
return ResolvedReference::ImplicitGlobal;
|
||||
}
|
||||
if index > 0 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(binding_id) = scope.get(symbol) {
|
||||
// Mark the binding as used.
|
||||
let context = self.execution_context();
|
||||
self.bindings[*binding_id].mark_used(self.scope_id, range, context);
|
||||
|
||||
// Mark any submodule aliases as used.
|
||||
if let Some(binding_id) = self.resolve_submodule(scope_id, *binding_id) {
|
||||
self.bindings[binding_id].mark_used(ScopeId::global(), range, context);
|
||||
}
|
||||
|
||||
// But if it's a type annotation, don't treat it as resolved, unless we're in a
|
||||
// forward reference. For example, given:
|
||||
//
|
||||
// ```python
|
||||
// name: str
|
||||
// print(name)
|
||||
// ```
|
||||
//
|
||||
// The `name` in `print(name)` should be treated as unresolved, but the `name` in
|
||||
// `name: str` should be treated as used.
|
||||
if !self.in_deferred_type_definition()
|
||||
&& self.bindings[*binding_id].kind.is_annotation()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return ResolvedReference::Resolved(*binding_id);
|
||||
}
|
||||
|
||||
// Allow usages of `__module__` and `__qualname__` within class scopes, e.g.:
|
||||
//
|
||||
// ```python
|
||||
// class Foo:
|
||||
// print(__qualname__)
|
||||
// ```
|
||||
//
|
||||
// Intentionally defer this check to _after_ the standard `scope.get` logic, so that
|
||||
// we properly attribute reads to overridden class members, e.g.:
|
||||
//
|
||||
// ```python
|
||||
// class Foo:
|
||||
// __qualname__ = "Bar"
|
||||
// print(__qualname__)
|
||||
// ```
|
||||
if index == 0 && scope.kind.is_class() {
|
||||
if matches!(symbol, "__module__" | "__qualname__") {
|
||||
return ResolvedReference::ImplicitGlobal;
|
||||
}
|
||||
}
|
||||
|
||||
seen_function |= scope.kind.is_function();
|
||||
import_starred = import_starred || scope.uses_star_imports();
|
||||
}
|
||||
|
||||
if import_starred {
|
||||
ResolvedReference::StarImport
|
||||
} else {
|
||||
ResolvedReference::NotFound
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a `BindingId`, return the `BindingId` of the submodule import that it aliases.
|
||||
fn resolve_submodule(&self, scope_id: ScopeId, binding_id: BindingId) -> Option<BindingId> {
|
||||
// If the name of a submodule import is the same as an alias of another import, and the
|
||||
// alias is used, then the submodule import should be marked as used too.
|
||||
//
|
||||
// For example, mark `pyarrow.csv` as used in:
|
||||
//
|
||||
// ```python
|
||||
// import pyarrow as pa
|
||||
// import pyarrow.csv
|
||||
// print(pa.csv.read_csv("test.csv"))
|
||||
// ```
|
||||
let (name, full_name) = match &self.bindings[binding_id].kind {
|
||||
BindingKind::Importation(Importation { name, full_name }) => (*name, *full_name),
|
||||
BindingKind::SubmoduleImportation(SubmoduleImportation { name, full_name }) => {
|
||||
(*name, *full_name)
|
||||
}
|
||||
BindingKind::FromImportation(FromImportation { name, full_name }) => {
|
||||
(*name, full_name.as_str())
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let has_alias = full_name
|
||||
.split('.')
|
||||
.last()
|
||||
.map(|segment| segment != name)
|
||||
.unwrap_or_default();
|
||||
if !has_alias {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.scopes[scope_id].get(full_name).copied()
|
||||
}
|
||||
|
||||
/// Resolves the [`Expr`] to a fully-qualified symbol-name, if `value` resolves to an imported
|
||||
/// or builtin symbol.
|
||||
///
|
||||
@@ -701,10 +837,24 @@ impl ContextFlags {
|
||||
}
|
||||
|
||||
/// A snapshot of the [`Context`] at a given point in the AST traversal.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Snapshot {
|
||||
scope_id: ScopeId,
|
||||
stmt_id: Option<NodeId>,
|
||||
definition_id: DefinitionId,
|
||||
flags: ContextFlags,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ResolvedReference {
|
||||
/// The reference is resolved to a specific binding.
|
||||
Resolved(BindingId),
|
||||
/// The reference is resolved to a context-specific, implicit global (e.g., `__class__` within
|
||||
/// a class scope).
|
||||
ImplicitGlobal,
|
||||
/// The reference is unresolved, but at least one of the containing scopes contains a star
|
||||
/// import.
|
||||
StarImport,
|
||||
/// The reference is definitively unresolved.
|
||||
NotFound,
|
||||
}
|
||||
|
||||
@@ -96,8 +96,10 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
|
||||
allowed_confusables: Some(Vec::default()),
|
||||
builtins: Some(Vec::default()),
|
||||
dummy_variable_rgx: Some(defaults::DUMMY_VARIABLE_RGX.as_str().to_string()),
|
||||
extend_fixable: Some(Vec::default()),
|
||||
extend_ignore: Some(Vec::default()),
|
||||
extend_select: Some(Vec::default()),
|
||||
extend_unfixable: Some(Vec::default()),
|
||||
external: Some(Vec::default()),
|
||||
ignore: Some(Vec::default()),
|
||||
line_length: Some(defaults::LINE_LENGTH),
|
||||
@@ -109,6 +111,7 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
|
||||
extend: None,
|
||||
extend_exclude: None,
|
||||
extend_include: None,
|
||||
extend_per_file_ignores: None,
|
||||
fix: None,
|
||||
fix_only: None,
|
||||
fixable: None,
|
||||
|
||||
@@ -232,13 +232,17 @@ Rule selection:
|
||||
--ignore <RULE_CODE>
|
||||
Comma-separated list of rule codes to disable
|
||||
--extend-select <RULE_CODE>
|
||||
Like --select, but adds additional rule codes on top of the selected ones
|
||||
Like --select, but adds additional rule codes on top of those already specified
|
||||
--per-file-ignores <PER_FILE_IGNORES>
|
||||
List of mappings from file pattern to code to exclude
|
||||
--extend-per-file-ignores <EXTEND_PER_FILE_IGNORES>
|
||||
Like `--per-file-ignores`, but adds additional ignores on top of those already specified
|
||||
--fixable <RULE_CODE>
|
||||
List of rule codes to treat as eligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
|
||||
--unfixable <RULE_CODE>
|
||||
List of rule codes to treat as ineligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
|
||||
--extend-fixable <RULE_CODE>
|
||||
Like --fixable, but adds additional rule codes on top of those already specified
|
||||
|
||||
File selection:
|
||||
--exclude <FILE_PATTERN> List of paths, used to omit files and/or directories from analysis
|
||||
|
||||
@@ -371,5 +371,5 @@ Ruff's color output is powered by the [`colored`](https://crates.io/crates/color
|
||||
attempts to automatically detect whether the output stream supports color. However, you can force
|
||||
colors off by setting the `NO_COLOR` environment variable to any value (e.g., `NO_COLOR=1`).
|
||||
|
||||
[`colored`](https://crates.io/crates/colored) also supports the the `CLICOLOR` and `CLICOLOR_FORCE`
|
||||
[`colored`](https://crates.io/crates/colored) also supports the `CLICOLOR` and `CLICOLOR_FORCE`
|
||||
environment variables (see the [spec](https://bixense.com/clicolors/)).
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user