Compare commits
108 Commits
v0.0.284
...
evanritten
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e268c91ab | ||
|
|
dbe62cc741 | ||
|
|
e38e8c0a51 | ||
|
|
036035bc50 | ||
|
|
97ae9e7433 | ||
|
|
98b9f2e705 | ||
|
|
6253d8e2c8 | ||
|
|
0a5be74be3 | ||
|
|
fdbb2fbdba | ||
|
|
d0b8e4f701 | ||
|
|
12f3c4c931 | ||
|
|
7ee2ae8395 | ||
|
|
95f78821ad | ||
|
|
daac31d2b9 | ||
|
|
86ccdcc9d9 | ||
|
|
e28858bb29 | ||
|
|
2d86e78bfc | ||
|
|
d9a81f4fbb | ||
|
|
897cce83b3 | ||
|
|
9bf6713b76 | ||
|
|
3f1658a25b | ||
|
|
097db2fcce | ||
|
|
a3d4f08f29 | ||
|
|
29c0b9f91c | ||
|
|
81b1176f99 | ||
|
|
b1c4c7be69 | ||
|
|
84d178a219 | ||
|
|
455db84a59 | ||
|
|
232b44a8ca | ||
|
|
e1e213decf | ||
|
|
5f709cd3e0 | ||
|
|
17e7eae2f9 | ||
|
|
7f7df852e8 | ||
|
|
9a0d2f5afd | ||
|
|
ebda5fcd99 | ||
|
|
b1870b2b16 | ||
|
|
a51d1ac980 | ||
|
|
1a52b548e7 | ||
|
|
a3bf6d9cb7 | ||
|
|
70696061cd | ||
|
|
cd634a9489 | ||
|
|
5ddf143cae | ||
|
|
46862473b9 | ||
|
|
96d310fbab | ||
|
|
09c8b17661 | ||
|
|
278a4f6e14 | ||
|
|
c3a9151eb5 | ||
|
|
3711f8ad59 | ||
|
|
a7cf8f0b77 | ||
|
|
40407dcce5 | ||
|
|
f16e780e0a | ||
|
|
fc0c9507d0 | ||
|
|
680d171ae5 | ||
|
|
01eceaf0dc | ||
|
|
910dbbd9b6 | ||
|
|
9584f613b9 | ||
|
|
c39bcbadff | ||
|
|
24f42f0894 | ||
|
|
51ae47ad56 | ||
|
|
1a9536c4e2 | ||
|
|
bf4c6473c8 | ||
|
|
768686148f | ||
|
|
eb24f5a0b9 | ||
|
|
446ceed1ad | ||
|
|
8660e5057c | ||
|
|
808e09180e | ||
|
|
dbf003fde4 | ||
|
|
010293ddcc | ||
|
|
4974964ad3 | ||
|
|
b49c80f8c8 | ||
|
|
c03e2acadb | ||
|
|
a1da9da0ef | ||
|
|
c6ad364d8b | ||
|
|
5b47350c25 | ||
|
|
e91caea490 | ||
|
|
53246b725e | ||
|
|
d616c9b870 | ||
|
|
7c4aa3948b | ||
|
|
b6d786fb10 | ||
|
|
0c9ded9d84 | ||
|
|
c434bdd2bd | ||
|
|
8b24238d19 | ||
|
|
f2939c678b | ||
|
|
b05574babd | ||
|
|
0ef6af807b | ||
|
|
f091b46497 | ||
|
|
2cedb401bd | ||
|
|
2e5c81b202 | ||
|
|
a12a71a845 | ||
|
|
cc151c35a8 | ||
|
|
563374503f | ||
|
|
95dea5c868 | ||
|
|
eb68addf97 | ||
|
|
9ff80a82b4 | ||
|
|
84ae00c395 | ||
|
|
1050c4e104 | ||
|
|
6706ae4828 | ||
|
|
dc3275fe7f | ||
|
|
50dab9cea6 | ||
|
|
4811af0f0b | ||
|
|
39beeb61f7 | ||
|
|
e2f7862404 | ||
|
|
ac5c8bb3b6 | ||
|
|
c1bc67686c | ||
|
|
7eea0e94a2 | ||
|
|
0252995973 | ||
|
|
627f475b91 | ||
|
|
395bb31247 |
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -328,7 +328,7 @@ jobs:
|
||||
name: "Formatter ecosystem and progress checks"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.formatter == 'true'
|
||||
if: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -338,6 +338,6 @@ jobs:
|
||||
- name: "Formatter progress"
|
||||
run: scripts/formatter_ecosystem_checks.sh
|
||||
- name: "Github step summary"
|
||||
run: grep "similarity index" target/progress_projects_log.txt | sort > $GITHUB_STEP_SUMMARY
|
||||
run: cat target/progress_projects_stats.txt > $GITHUB_STEP_SUMMARY
|
||||
- name: "Remove checkouts from cache"
|
||||
run: rm -r target/progress_projects
|
||||
|
||||
@@ -571,7 +571,7 @@ An alternative is to convert the perf data to `flamegraph.svg` using
|
||||
[flamegraph](https://github.com/flamegraph-rs/flamegraph) (`cargo install flamegraph`):
|
||||
|
||||
```shell
|
||||
flamegraph --perfdata perf.data
|
||||
flamegraph --perfdata perf.data --no-inline
|
||||
```
|
||||
|
||||
#### Mac
|
||||
|
||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -14,6 +14,18 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
@@ -991,6 +1003,16 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imara-diff"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"hashbrown 0.12.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imperative"
|
||||
version = "1.0.4"
|
||||
@@ -2119,6 +2141,7 @@ dependencies = [
|
||||
"ruff",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_index",
|
||||
"ruff_python_parser",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2197,6 +2220,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"ignore",
|
||||
"imara-diff",
|
||||
"indicatif",
|
||||
"indoc",
|
||||
"itertools",
|
||||
@@ -2330,6 +2354,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2353,7 +2378,6 @@ dependencies = [
|
||||
"is-macro",
|
||||
"itertools",
|
||||
"lexical-parse-float",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"unic-ucd-category",
|
||||
@@ -2400,6 +2424,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"ruff_index",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
|
||||
@@ -49,6 +49,7 @@ toml = { version = "0.7.2" }
|
||||
tracing = "0.1.37"
|
||||
tracing-indicatif = "0.3.4"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
unicode-width = "0.1.10"
|
||||
wsl = { version = "0.1.0" }
|
||||
|
||||
# v1.0.1
|
||||
|
||||
@@ -30,7 +30,7 @@ An extremely fast Python linter, written in Rust.
|
||||
- 🤝 Python 3.11 compatibility
|
||||
- 📦 Built-in caching, to avoid re-analyzing unchanged files
|
||||
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
|
||||
- 📏 Over [500 built-in rules](https://beta.ruff.rs/docs/rules/)
|
||||
- 📏 Over [600 built-in rules](https://beta.ruff.rs/docs/rules/)
|
||||
- ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the
|
||||
built-in Flake8 rule set
|
||||
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
|
||||
@@ -233,7 +233,7 @@ linting command.
|
||||
|
||||
<!-- Begin section: Rules -->
|
||||
|
||||
**Ruff supports over 500 lint rules**, many of which are inspired by popular tools like Flake8,
|
||||
**Ruff supports over 600 lint rules**, many of which are inspired by popular tools like Flake8,
|
||||
isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in
|
||||
Rust as a first-party feature.
|
||||
|
||||
|
||||
@@ -62,8 +62,6 @@ quick-junit = { version = "0.3.2" }
|
||||
regex = { workspace = true }
|
||||
result-like = { version = "0.4.6" }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
|
||||
schemars = { workspace = true, optional = true }
|
||||
semver = { version = "1.0.16" }
|
||||
serde = { workspace = true }
|
||||
@@ -77,7 +75,7 @@ strum_macros = { workspace = true }
|
||||
thiserror = { version = "1.0.43" }
|
||||
toml = { workspace = true }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unicode-width = { version = "0.1.10" }
|
||||
unicode-width = { workspace = true }
|
||||
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
|
||||
wsl = { version = "0.1.0" }
|
||||
|
||||
|
||||
@@ -68,6 +68,20 @@ def this_is_also_wrong(value={}):
|
||||
...
|
||||
|
||||
|
||||
class Foo:
|
||||
@staticmethod
|
||||
def this_is_also_wrong_and_more_indented(value={}):
|
||||
pass
|
||||
|
||||
|
||||
def multiline_arg_wrong(value={
|
||||
|
||||
}):
|
||||
...
|
||||
|
||||
def single_line_func_wrong(value = {}): ...
|
||||
|
||||
|
||||
def and_this(value=set()):
|
||||
...
|
||||
|
||||
@@ -261,3 +275,32 @@ def mutable_annotations(
|
||||
d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
"""Docstring"""
|
||||
|
||||
|
||||
def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
"""Docstring"""
|
||||
...
|
||||
|
||||
|
||||
def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
"""Docstring"""; ...
|
||||
|
||||
|
||||
def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
"""Docstring"""; \
|
||||
...
|
||||
|
||||
|
||||
def single_line_func_wrong(value: dict[str, str] = {
|
||||
# This is a comment
|
||||
}):
|
||||
"""Docstring"""
|
||||
|
||||
|
||||
def single_line_func_wrong(value: dict[str, str] = {}) \
|
||||
: \
|
||||
"""Docstring"""
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import builtins
|
||||
from typing import Union
|
||||
|
||||
|
||||
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
x: type[int] | type[str] | type[float]
|
||||
y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
@@ -9,7 +8,9 @@ z: Union[type[float], type[complex]]
|
||||
z: Union[type[float, int], type[complex]]
|
||||
|
||||
|
||||
def func(arg: type[int] | str | type[float]) -> None: ...
|
||||
def func(arg: type[int] | str | type[float]) -> None:
|
||||
...
|
||||
|
||||
|
||||
# OK
|
||||
x: type[int, str, float]
|
||||
@@ -17,4 +18,14 @@ y: builtins.type[int, str, complex]
|
||||
z: Union[float, complex]
|
||||
|
||||
|
||||
def func(arg: type[int, float] | str) -> None: ...
|
||||
def func(arg: type[int, float] | str) -> None:
|
||||
...
|
||||
|
||||
|
||||
# OK
|
||||
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
|
||||
|
||||
|
||||
def func():
|
||||
# PYI055
|
||||
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import builtins
|
||||
from typing import Union
|
||||
|
||||
|
||||
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
x: type[int] | type[str] | type[float]
|
||||
y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
z: Union[type[float], type[complex]]
|
||||
z: Union[type[float, int], type[complex]]
|
||||
|
||||
|
||||
def func(arg: type[int] | str | type[float]) -> None: ...
|
||||
|
||||
# OK
|
||||
@@ -16,5 +14,11 @@ x: type[int, str, float]
|
||||
y: builtins.type[int, str, complex]
|
||||
z: Union[float, complex]
|
||||
|
||||
|
||||
def func(arg: type[int, float] | str) -> None: ...
|
||||
|
||||
# OK
|
||||
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
|
||||
|
||||
def func():
|
||||
# PYI055
|
||||
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
|
||||
|
||||
@@ -80,3 +80,15 @@ class Test(unittest.TestCase):
|
||||
|
||||
def test_assert_not_regexp_matches(self):
|
||||
self.assertNotRegex("abc", r"abc") # Error
|
||||
|
||||
def test_fail_if(self):
|
||||
self.failIf("abc") # Error
|
||||
|
||||
def test_fail_unless(self):
|
||||
self.failUnless("abc") # Error
|
||||
|
||||
def test_fail_unless_equal(self):
|
||||
self.failUnlessEqual(1, 2) # Error
|
||||
|
||||
def test_fail_if_equal(self):
|
||||
self.failIfEqual(1, 2) # Error
|
||||
|
||||
26
crates/ruff/resources/test/fixtures/flake8_pytest_style/PT014.py
vendored
Normal file
26
crates/ruff/resources/test/fixtures/flake8_pytest_style/PT014.py
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("x", [1, 1, 2])
|
||||
def test_error_literal(x):
|
||||
...
|
||||
|
||||
|
||||
a = 1
|
||||
b = 2
|
||||
c = 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("x", [a, a, b, b, b, c])
|
||||
def test_error_expr_simple(x):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize("x", [(a, b), (a, b), (b, c)])
|
||||
def test_error_expr_complex(x):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize("x", [1, 2])
|
||||
def test_ok(x):
|
||||
...
|
||||
48
crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py
vendored
Normal file
48
crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import unittest
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
def test_errors(self):
|
||||
with self.assertRaises(ValueError):
|
||||
raise ValueError
|
||||
with self.assertRaises(expected_exception=ValueError):
|
||||
raise ValueError
|
||||
|
||||
with self.failUnlessRaises(ValueError):
|
||||
raise ValueError
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "test"):
|
||||
raise ValueError("test")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, expected_regex="test"):
|
||||
raise ValueError("test")
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
expected_exception=ValueError, expected_regex="test"
|
||||
):
|
||||
raise ValueError("test")
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
expected_regex="test", expected_exception=ValueError
|
||||
):
|
||||
raise ValueError("test")
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "test"):
|
||||
raise ValueError("test")
|
||||
|
||||
def test_unfixable_errors(self):
|
||||
with self.assertRaises(ValueError, msg="msg"):
|
||||
raise ValueError
|
||||
|
||||
with self.assertRaises(
|
||||
# comment
|
||||
ValueError
|
||||
):
|
||||
raise ValueError
|
||||
|
||||
with (
|
||||
self
|
||||
# comment
|
||||
.assertRaises(ValueError)
|
||||
):
|
||||
raise ValueError
|
||||
12
crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_1.py
vendored
Normal file
12
crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_1.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import unittest
|
||||
import pytest
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
def test_pytest_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
raise ValueError
|
||||
|
||||
def test_errors(self):
|
||||
with self.assertRaises(ValueError):
|
||||
raise ValueError
|
||||
@@ -73,3 +73,7 @@ print(foo.__dict__)
|
||||
print(foo.__str__())
|
||||
print(foo().__class__)
|
||||
print(foo._asdict())
|
||||
|
||||
import os
|
||||
|
||||
os._exit()
|
||||
|
||||
31
crates/ruff/resources/test/fixtures/flake8_tidy_imports/TID253.py
vendored
Normal file
31
crates/ruff/resources/test/fixtures/flake8_tidy_imports/TID253.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
## Banned modules ##
|
||||
import torch
|
||||
|
||||
from torch import *
|
||||
|
||||
from tensorflow import a, b, c
|
||||
|
||||
import torch as torch_wearing_a_trenchcoat
|
||||
|
||||
# this should count as module level
|
||||
x = 1; import tensorflow
|
||||
|
||||
# banning a module also bans any submodules
|
||||
import torch.foo.bar
|
||||
|
||||
from tensorflow.foo import bar
|
||||
|
||||
from torch.foo.bar import *
|
||||
|
||||
# unlike TID251, inline imports are *not* banned
|
||||
def my_cool_function():
|
||||
import tensorflow.foo.bar
|
||||
|
||||
def another_cool_function():
|
||||
from torch.foo import bar
|
||||
|
||||
def import_alias():
|
||||
from torch.foo import bar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import torch
|
||||
12
crates/ruff/resources/test/fixtures/flake8_type_checking/runtime_evaluated_base_classes_4.py
vendored
Normal file
12
crates/ruff/resources/test/fixtures/flake8_type_checking/runtime_evaluated_base_classes_4.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
class Birthday(DeclarativeBase):
|
||||
|
||||
__tablename__ = "birthday"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
day: Mapped[date]
|
||||
@@ -202,3 +202,14 @@ class C:
|
||||
###
|
||||
def f(x: None) -> None:
|
||||
_ = cast(Any, _identity)(x=x)
|
||||
|
||||
###
|
||||
# Unused arguments with `locals`.
|
||||
###
|
||||
def f(bar: str):
|
||||
print(locals())
|
||||
|
||||
|
||||
class C:
|
||||
def __init__(self, x) -> None:
|
||||
print(locals())
|
||||
|
||||
6
crates/ruff/resources/test/fixtures/isort/required_imports/comments_and_newlines.py
vendored
Normal file
6
crates/ruff/resources/test/fixtures/isort/required_imports/comments_and_newlines.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# A copyright notice could go here
|
||||
|
||||
# A linter directive could go here
|
||||
|
||||
x = 1
|
||||
@@ -21,3 +21,29 @@ while i < 10:
|
||||
print("error")
|
||||
|
||||
i += 1
|
||||
|
||||
# OK - no other way to write this
|
||||
for i in range(10):
|
||||
try:
|
||||
print(f"{i}")
|
||||
break
|
||||
except:
|
||||
print("error")
|
||||
|
||||
# OK - no other way to write this
|
||||
for i in range(10):
|
||||
try:
|
||||
print(f"{i}")
|
||||
continue
|
||||
except:
|
||||
print("error")
|
||||
|
||||
|
||||
# OK - no other way to write this
|
||||
for i in range(10):
|
||||
try:
|
||||
print(f"{i}")
|
||||
if i > 0:
|
||||
break
|
||||
except:
|
||||
print("error")
|
||||
|
||||
@@ -30,3 +30,10 @@ def foo() -> None:
|
||||
|
||||
if __name__ == "__main__":
|
||||
import g
|
||||
|
||||
import h; import i
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import j; \
|
||||
import k
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
a = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
a = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
# aaaa
|
||||
# aaaaa
|
||||
# a
|
||||
# a
|
||||
# aa
|
||||
# aaa
|
||||
# aaaa
|
||||
# a
|
||||
# aa
|
||||
# aaa
|
||||
|
||||
b = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
b = """ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
|
||||
c = """2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
c = """2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
|
||||
d = """💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
d = """💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A67ß9💣2ℝ4A6"""
|
||||
if True: # noqa: E501
|
||||
[12]
|
||||
[12 ]
|
||||
[1,2]
|
||||
[1, 2]
|
||||
|
||||
@@ -61,3 +61,30 @@ if x == types.X:
|
||||
|
||||
#: E721
|
||||
assert type(res) is int
|
||||
|
||||
|
||||
class Foo:
|
||||
def asdf(self, value: str | None):
|
||||
#: E721
|
||||
if type(value) is str:
|
||||
...
|
||||
|
||||
|
||||
class Foo:
|
||||
def type(self):
|
||||
pass
|
||||
|
||||
def asdf(self, value: str | None):
|
||||
#: E721
|
||||
if type(value) is str:
|
||||
...
|
||||
|
||||
|
||||
class Foo:
|
||||
def asdf(self, value: str | None):
|
||||
def type():
|
||||
pass
|
||||
|
||||
# Okay
|
||||
if type(value) is str:
|
||||
...
|
||||
|
||||
@@ -145,3 +145,9 @@ def f() -> None:
|
||||
obj = Foo()
|
||||
obj.do_thing()
|
||||
|
||||
|
||||
def f():
|
||||
try:
|
||||
pass
|
||||
except Exception as _:
|
||||
pass
|
||||
|
||||
46
crates/ruff/resources/test/fixtures/pylint/bad_dunder_method_name.py
vendored
Normal file
46
crates/ruff/resources/test/fixtures/pylint/bad_dunder_method_name.py
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
class Apples:
|
||||
def _init_(self): # [bad-dunder-name]
|
||||
pass
|
||||
|
||||
def __hello__(self): # [bad-dunder-name]
|
||||
print("hello")
|
||||
|
||||
def __init_(self): # [bad-dunder-name]
|
||||
# author likely unintentionally misspelled the correct init dunder.
|
||||
pass
|
||||
|
||||
def _init_(self): # [bad-dunder-name]
|
||||
# author likely unintentionally misspelled the correct init dunder.
|
||||
pass
|
||||
|
||||
def ___neg__(self): # [bad-dunder-name]
|
||||
# author likely accidentally added an additional `_`
|
||||
pass
|
||||
|
||||
def __inv__(self): # [bad-dunder-name]
|
||||
# author likely meant to call the invert dunder method
|
||||
pass
|
||||
|
||||
def hello(self):
|
||||
print("hello")
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def init(self):
|
||||
# valid name even though someone could accidentally mean __init__
|
||||
pass
|
||||
|
||||
def _protected_method(self):
|
||||
print("Protected")
|
||||
|
||||
def __private_method(self):
|
||||
print("Private")
|
||||
|
||||
@property
|
||||
def __doc__(self):
|
||||
return "Docstring"
|
||||
|
||||
|
||||
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
|
||||
...
|
||||
@@ -13,6 +13,7 @@ print("foo %(foo)d bar %(bar)d" % {"foo": "1", "bar": "2"})
|
||||
"%(key)d" % {"key": []}
|
||||
print("%d" % ("%s" % ("nested",),))
|
||||
"%d" % ((1, 2, 3),)
|
||||
"%d" % (1 if x > 0 else [])
|
||||
|
||||
# False negatives
|
||||
WORD = "abc"
|
||||
@@ -55,3 +56,4 @@ r'\%03o' % (ord(c),)
|
||||
"%d" % (len(foo),)
|
||||
'(%r, %r, %r, %r)' % (hostname, address, username, '$PASSWORD')
|
||||
'%r' % ({'server_school_roles': server_school_roles, 'is_school_multiserver_domain': is_school_multiserver_domain}, )
|
||||
"%d" % (1 if x > 0 else 2)
|
||||
|
||||
@@ -10,3 +10,4 @@ os.getenv("AA", "GOOD" + "BAD")
|
||||
os.getenv("AA", "GOOD" + 1)
|
||||
os.getenv("AA", "GOOD %s" % "BAD")
|
||||
os.getenv("B", Z)
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ os.getenv(key="foo", default="bar")
|
||||
os.getenv(key=f"foo", default="bar")
|
||||
os.getenv(key="foo" + "bar", default=1)
|
||||
os.getenv(key=1 + "bar", default=1) # [invalid-envvar-value]
|
||||
os.getenv("PATH_TEST" if using_clear_path else "PATH_ORIG")
|
||||
os.getenv(1 if using_clear_path else "PATH_ORIG")
|
||||
|
||||
AA = "aa"
|
||||
os.getenv(AA)
|
||||
|
||||
@@ -19,6 +19,10 @@ logging.error("Example log %s, %s", "foo", "bar", "baz", **kwargs)
|
||||
# do not handle keyword arguments
|
||||
logging.error("%(objects)d modifications: %(modifications)d errors: %(errors)d")
|
||||
|
||||
logging.info(msg="Hello %s")
|
||||
|
||||
logging.info(msg="Hello %s %s")
|
||||
|
||||
import warning
|
||||
|
||||
warning.warning("Hello %s %s", "World!")
|
||||
|
||||
@@ -15,6 +15,10 @@ logging.error("Example log %s, %s", "foo", "bar", "baz", **kwargs)
|
||||
# do not handle keyword arguments
|
||||
logging.error("%(objects)d modifications: %(modifications)d errors: %(errors)d", {"objects": 1, "modifications": 1, "errors": 1})
|
||||
|
||||
logging.info(msg="Hello")
|
||||
|
||||
logging.info(msg="Hello", something="else")
|
||||
|
||||
import warning
|
||||
|
||||
warning.warning("Hello %s", "World!", "again")
|
||||
|
||||
13
crates/ruff/resources/test/fixtures/pylint/subprocess_run_without_check.py
vendored
Normal file
13
crates/ruff/resources/test/fixtures/pylint/subprocess_run_without_check.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import subprocess
|
||||
|
||||
# Errors.
|
||||
subprocess.run("ls")
|
||||
subprocess.run("ls", shell=True)
|
||||
|
||||
# Non-errors.
|
||||
subprocess.run("ls", check=True)
|
||||
subprocess.run("ls", check=False)
|
||||
subprocess.run("ls", shell=True, check=True)
|
||||
subprocess.run("ls", shell=True, check=False)
|
||||
foo.run("ls") # Not a subprocess.run call.
|
||||
subprocess.bar("ls") # Not a subprocess.run call.
|
||||
14
crates/ruff/resources/test/fixtures/ruff/RUF017.py
vendored
Normal file
14
crates/ruff/resources/test/fixtures/ruff/RUF017.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
x = [1, 2, 3]
|
||||
y = [4, 5, 6]
|
||||
|
||||
# RUF017
|
||||
sum([x, y], start=[])
|
||||
sum([x, y], [])
|
||||
sum([[1, 2, 3], [4, 5, 6]], start=[])
|
||||
sum([[1, 2, 3], [4, 5, 6]], [])
|
||||
sum([[1, 2, 3], [4, 5, 6]],
|
||||
[])
|
||||
|
||||
# OK
|
||||
sum([x, y])
|
||||
sum([[1, 2, 3], [4, 5, 6]])
|
||||
@@ -52,3 +52,7 @@ def good(a: int):
|
||||
def another_good(a):
|
||||
if a % 2 == 0:
|
||||
raise GoodArgCantBeEven(a)
|
||||
|
||||
|
||||
def another_good():
|
||||
raise NotImplementedError("This is acceptable too")
|
||||
|
||||
@@ -209,14 +209,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
range: _,
|
||||
})
|
||||
| Stmt::TryStar(ast::StmtTryStar {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
range: _,
|
||||
..
|
||||
}) => {
|
||||
if is_only(body, child)
|
||||
|| is_only(orelse, child)
|
||||
|
||||
@@ -16,9 +16,15 @@ pub(crate) fn bindings(checker: &mut Checker) {
|
||||
return;
|
||||
}
|
||||
|
||||
for binding in checker.semantic.bindings.iter() {
|
||||
for binding in &*checker.semantic.bindings {
|
||||
if checker.enabled(Rule::UnusedVariable) {
|
||||
if binding.kind.is_bound_exception() && !binding.is_used() {
|
||||
if binding.kind.is_bound_exception()
|
||||
&& !binding.is_used()
|
||||
&& !checker
|
||||
.settings
|
||||
.dummy_variable_rgx
|
||||
.is_match(binding.name(checker.locator))
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
pyflakes::rules::UnusedVariable {
|
||||
name: binding.name(checker.locator).to_string(),
|
||||
|
||||
@@ -171,7 +171,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
expr.start(),
|
||||
));
|
||||
|
||||
if pydocstyle::helpers::should_ignore_docstring(contents) {
|
||||
if pydocstyle::helpers::should_ignore_docstring(expr) {
|
||||
#[allow(deprecated)]
|
||||
let location = checker.locator.compute_source_location(expr.start());
|
||||
warn_user!(
|
||||
|
||||
@@ -261,7 +261,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pylint::rules::load_before_global_declaration(checker, id, expr);
|
||||
}
|
||||
}
|
||||
Expr::Attribute(ast::ExprAttribute { attr, value, .. }) => {
|
||||
Expr::Attribute(attribute) => {
|
||||
// Ex) typing.List[...]
|
||||
if checker.any_enabled(&[
|
||||
Rule::FutureRewritableTypeAnnotation,
|
||||
@@ -323,7 +323,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::CollectionsNamedTuple) {
|
||||
flake8_pyi::rules::collections_named_tuple(checker, expr);
|
||||
}
|
||||
pandas_vet::rules::attr(checker, attr, value, expr);
|
||||
pandas_vet::rules::attr(checker, attribute);
|
||||
}
|
||||
Expr::Call(
|
||||
call @ ast::ExprCall {
|
||||
@@ -418,9 +418,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
|
||||
if checker.enabled(Rule::BadStringFormatCharacter) {
|
||||
pylint::rules::bad_string_format_character::call(
|
||||
checker,
|
||||
val.as_str(),
|
||||
location,
|
||||
checker, val, location,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -675,10 +673,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::BooleanPositionalValueInFunctionCall) {
|
||||
flake8_boolean_trap::rules::check_boolean_positional_value_in_function_call(
|
||||
checker, args, func,
|
||||
);
|
||||
if checker.enabled(Rule::BooleanPositionalValueInCall) {
|
||||
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, args, func);
|
||||
}
|
||||
if checker.enabled(Rule::Debugger) {
|
||||
flake8_debugger::rules::debugger_call(checker, expr, func);
|
||||
@@ -760,9 +756,19 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::PytestUnittestRaisesAssertion) {
|
||||
if let Some(diagnostic) =
|
||||
flake8_pytest_style::rules::unittest_raises_assertion(checker, call)
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::SubprocessPopenPreexecFn) {
|
||||
pylint::rules::subprocess_popen_preexec_fn(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::SubprocessRunWithoutCheck) {
|
||||
pylint::rules::subprocess_run_without_check(checker, call);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::PytestRaisesWithoutException,
|
||||
Rule::PytestRaisesTooBroad,
|
||||
@@ -867,6 +873,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnsupportedMethodCallOnAll) {
|
||||
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
|
||||
}
|
||||
if checker.enabled(Rule::QuadraticListSummation) {
|
||||
ruff::rules::quadratic_list_summation(checker, call);
|
||||
}
|
||||
}
|
||||
Expr::Dict(ast::ExprDict {
|
||||
keys,
|
||||
@@ -915,7 +924,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pylint::rules::await_outside_async(checker, expr);
|
||||
}
|
||||
}
|
||||
Expr::FString(ast::ExprFString { values, range: _ }) => {
|
||||
Expr::FString(ast::ExprFString { values, .. }) => {
|
||||
if checker.enabled(Rule::FStringMissingPlaceholders) {
|
||||
pyflakes::rules::f_string_missing_placeholders(expr, values, checker);
|
||||
}
|
||||
@@ -942,7 +951,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
range: _,
|
||||
}) => {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
value: Constant::Str(ast::StringConstant { value, .. }),
|
||||
..
|
||||
}) = left.as_ref()
|
||||
{
|
||||
@@ -1095,22 +1104,15 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op,
|
||||
operand,
|
||||
range: _,
|
||||
}) => {
|
||||
let check_not_in = checker.enabled(Rule::NotInTest);
|
||||
let check_not_is = checker.enabled(Rule::NotIsTest);
|
||||
if check_not_in || check_not_is {
|
||||
pycodestyle::rules::not_tests(
|
||||
checker,
|
||||
expr,
|
||||
*op,
|
||||
operand,
|
||||
check_not_in,
|
||||
check_not_is,
|
||||
);
|
||||
Expr::UnaryOp(
|
||||
unary_op @ ast::ExprUnaryOp {
|
||||
op,
|
||||
operand,
|
||||
range: _,
|
||||
},
|
||||
) => {
|
||||
if checker.any_enabled(&[Rule::NotInTest, Rule::NotIsTest]) {
|
||||
pycodestyle::rules::not_tests(checker, unary_op);
|
||||
}
|
||||
if checker.enabled(Rule::UnaryPrefixIncrementDecrement) {
|
||||
flake8_bugbear::rules::unary_prefix_increment_decrement(
|
||||
@@ -1135,18 +1137,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
range: _,
|
||||
},
|
||||
) => {
|
||||
let check_none_comparisons = checker.enabled(Rule::NoneComparison);
|
||||
let check_true_false_comparisons = checker.enabled(Rule::TrueFalseComparison);
|
||||
if check_none_comparisons || check_true_false_comparisons {
|
||||
pycodestyle::rules::literal_comparisons(
|
||||
checker,
|
||||
expr,
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
check_none_comparisons,
|
||||
check_true_false_comparisons,
|
||||
);
|
||||
if checker.any_enabled(&[Rule::NoneComparison, Rule::TrueFalseComparison]) {
|
||||
pycodestyle::rules::literal_comparisons(checker, compare);
|
||||
}
|
||||
if checker.enabled(Rule::IsLiteral) {
|
||||
pyflakes::rules::invalid_literal_comparison(checker, left, ops, comparators, expr);
|
||||
|
||||
@@ -6,9 +6,6 @@ use crate::rules::{flake8_bugbear, flake8_pyi, ruff};
|
||||
|
||||
/// Run lint rules over a [`Parameters`] syntax node.
|
||||
pub(crate) fn parameters(parameters: &Parameters, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::MutableArgumentDefault) {
|
||||
flake8_bugbear::rules::mutable_argument_default(checker, parameters);
|
||||
}
|
||||
if checker.enabled(Rule::FunctionCallInDefaultArgument) {
|
||||
flake8_bugbear::rules::function_call_in_argument_default(checker, parameters);
|
||||
}
|
||||
|
||||
@@ -69,16 +69,18 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||
is_async,
|
||||
name,
|
||||
decorator_list,
|
||||
returns,
|
||||
parameters,
|
||||
body,
|
||||
type_params,
|
||||
range: _,
|
||||
}) => {
|
||||
Stmt::FunctionDef(
|
||||
function_def @ ast::StmtFunctionDef {
|
||||
is_async,
|
||||
name,
|
||||
decorator_list,
|
||||
returns,
|
||||
parameters,
|
||||
body,
|
||||
type_params,
|
||||
range: _,
|
||||
},
|
||||
) => {
|
||||
if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
|
||||
flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list);
|
||||
}
|
||||
@@ -204,6 +206,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::CachedInstanceMethod) {
|
||||
flake8_bugbear::rules::cached_instance_method(checker, decorator_list);
|
||||
}
|
||||
if checker.enabled(Rule::MutableArgumentDefault) {
|
||||
flake8_bugbear::rules::mutable_argument_default(checker, function_def);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::UnnecessaryReturnNone,
|
||||
Rule::ImplicitReturnValue,
|
||||
@@ -295,6 +300,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.any_enabled(&[
|
||||
Rule::PytestParametrizeNamesWrongType,
|
||||
Rule::PytestParametrizeValuesWrongType,
|
||||
Rule::PytestDuplicateParametrizeTestCases,
|
||||
]) {
|
||||
flake8_pytest_style::rules::parametrize(checker, decorator_list);
|
||||
}
|
||||
@@ -304,16 +310,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
]) {
|
||||
flake8_pytest_style::rules::marks(checker, decorator_list);
|
||||
}
|
||||
if checker.enabled(Rule::BooleanPositionalArgInFunctionDefinition) {
|
||||
flake8_boolean_trap::rules::check_positional_boolean_in_def(
|
||||
if checker.enabled(Rule::BooleanTypeHintPositionalArgument) {
|
||||
flake8_boolean_trap::rules::boolean_type_hint_positional_argument(
|
||||
checker,
|
||||
name,
|
||||
decorator_list,
|
||||
parameters,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::BooleanDefaultValueInFunctionDefinition) {
|
||||
flake8_boolean_trap::rules::check_boolean_default_value_in_function_definition(
|
||||
if checker.enabled(Rule::BooleanDefaultValuePositionalArgument) {
|
||||
flake8_boolean_trap::rules::boolean_default_value_positional_argument(
|
||||
checker,
|
||||
name,
|
||||
decorator_list,
|
||||
@@ -506,17 +512,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::SingleStringSlots) {
|
||||
pylint::rules::single_string_slots(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::BadDunderMethodName) {
|
||||
pylint::rules::bad_dunder_method_name(checker, body);
|
||||
}
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
if checker.enabled(Rule::MultipleImportsOnOneLine) {
|
||||
pycodestyle::rules::multiple_imports_on_one_line(checker, stmt, names);
|
||||
}
|
||||
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
|
||||
pycodestyle::rules::module_import_not_at_top_of_file(
|
||||
checker,
|
||||
stmt,
|
||||
checker.locator,
|
||||
);
|
||||
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::GlobalStatement) {
|
||||
for name in names {
|
||||
@@ -552,12 +557,29 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::BannedApi) {
|
||||
flake8_tidy_imports::rules::name_or_parent_is_banned(
|
||||
flake8_tidy_imports::rules::banned_api(
|
||||
checker,
|
||||
&alias.name,
|
||||
alias,
|
||||
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
|
||||
flake8_tidy_imports::matchers::MatchNameOrParent {
|
||||
module: &alias.name,
|
||||
},
|
||||
),
|
||||
&alias,
|
||||
);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::BannedModuleLevelImports) {
|
||||
flake8_tidy_imports::rules::banned_module_level_imports(
|
||||
checker,
|
||||
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
|
||||
flake8_tidy_imports::matchers::MatchNameOrParent {
|
||||
module: &alias.name,
|
||||
},
|
||||
),
|
||||
&alias,
|
||||
);
|
||||
}
|
||||
|
||||
if !checker.source_type.is_stub() {
|
||||
if checker.enabled(Rule::UselessImportAlias) {
|
||||
pylint::rules::useless_import_alias(checker, alias);
|
||||
@@ -672,11 +694,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
let module = module.as_deref();
|
||||
let level = level.map(|level| level.to_u32());
|
||||
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
|
||||
pycodestyle::rules::module_import_not_at_top_of_file(
|
||||
checker,
|
||||
stmt,
|
||||
checker.locator,
|
||||
);
|
||||
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::GlobalStatement) {
|
||||
for name in names {
|
||||
@@ -712,16 +730,56 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if let Some(module) =
|
||||
helpers::resolve_imported_module_path(level, module, checker.module_path)
|
||||
{
|
||||
flake8_tidy_imports::rules::name_or_parent_is_banned(checker, &module, stmt);
|
||||
flake8_tidy_imports::rules::banned_api(
|
||||
checker,
|
||||
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
|
||||
flake8_tidy_imports::matchers::MatchNameOrParent { module: &module },
|
||||
),
|
||||
&stmt,
|
||||
);
|
||||
|
||||
for alias in names {
|
||||
if &alias.name == "*" {
|
||||
continue;
|
||||
}
|
||||
flake8_tidy_imports::rules::name_is_banned(
|
||||
flake8_tidy_imports::rules::banned_api(
|
||||
checker,
|
||||
format!("{module}.{}", alias.name),
|
||||
alias,
|
||||
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchName(
|
||||
flake8_tidy_imports::matchers::MatchName {
|
||||
module: &module,
|
||||
member: &alias.name,
|
||||
},
|
||||
),
|
||||
&alias,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::BannedModuleLevelImports) {
|
||||
if let Some(module) =
|
||||
helpers::resolve_imported_module_path(level, module, checker.module_path)
|
||||
{
|
||||
flake8_tidy_imports::rules::banned_module_level_imports(
|
||||
checker,
|
||||
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
|
||||
flake8_tidy_imports::matchers::MatchNameOrParent { module: &module },
|
||||
),
|
||||
&stmt,
|
||||
);
|
||||
|
||||
for alias in names {
|
||||
if &alias.name == "*" {
|
||||
continue;
|
||||
}
|
||||
flake8_tidy_imports::rules::banned_module_level_imports(
|
||||
checker,
|
||||
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchName(
|
||||
flake8_tidy_imports::matchers::MatchName {
|
||||
module: &module,
|
||||
member: &alias.name,
|
||||
},
|
||||
),
|
||||
&alias,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1168,14 +1226,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
range: _,
|
||||
})
|
||||
| Stmt::TryStar(ast::StmtTryStar {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
range: _,
|
||||
..
|
||||
}) => {
|
||||
if checker.enabled(Rule::JumpStatementInFinally) {
|
||||
flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody);
|
||||
|
||||
@@ -599,14 +599,7 @@ where
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
range: _,
|
||||
})
|
||||
| Stmt::TryStar(ast::StmtTryStar {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
range: _,
|
||||
..
|
||||
}) => {
|
||||
let mut handled_exceptions = Exceptions::empty();
|
||||
for type_ in extract_handled_exceptions(handlers) {
|
||||
@@ -1275,7 +1268,7 @@ where
|
||||
|
||||
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
|
||||
match format_spec {
|
||||
Expr::FString(ast::ExprFString { values, range: _ }) => {
|
||||
Expr::FString(ast::ExprFString { values, .. }) => {
|
||||
for value in values {
|
||||
self.visit_expr(value);
|
||||
}
|
||||
|
||||
@@ -226,8 +226,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "W0711") => (RuleGroup::Unspecified, rules::pylint::rules::BinaryOpException),
|
||||
(Pylint, "W1508") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidEnvvarDefault),
|
||||
(Pylint, "W1509") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessPopenPreexecFn),
|
||||
(Pylint, "W1510") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessRunWithoutCheck),
|
||||
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
|
||||
(Pylint, "W2901") => (RuleGroup::Unspecified, rules::pylint::rules::RedefinedLoopName),
|
||||
(Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName),
|
||||
(Pylint, "W3301") => (RuleGroup::Unspecified, rules::pylint::rules::NestedMinMax),
|
||||
|
||||
// flake8-async
|
||||
@@ -309,6 +311,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
// flake8-tidy-imports
|
||||
(Flake8TidyImports, "251") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::BannedApi),
|
||||
(Flake8TidyImports, "252") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::RelativeImports),
|
||||
(Flake8TidyImports, "253") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::BannedModuleLevelImports),
|
||||
|
||||
// flake8-return
|
||||
(Flake8Return, "501") => (RuleGroup::Unspecified, rules::flake8_return::rules::UnnecessaryReturnNone),
|
||||
@@ -565,9 +568,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Bandit, "701") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::Jinja2AutoescapeFalse),
|
||||
|
||||
// flake8-boolean-trap
|
||||
(Flake8BooleanTrap, "001") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanPositionalArgInFunctionDefinition),
|
||||
(Flake8BooleanTrap, "002") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanDefaultValueInFunctionDefinition),
|
||||
(Flake8BooleanTrap, "003") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanPositionalValueInFunctionCall),
|
||||
(Flake8BooleanTrap, "001") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanTypeHintPositionalArgument),
|
||||
(Flake8BooleanTrap, "002") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanDefaultValuePositionalArgument),
|
||||
(Flake8BooleanTrap, "003") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanPositionalValueInCall),
|
||||
|
||||
// flake8-unused-arguments
|
||||
(Flake8UnusedArguments, "001") => (RuleGroup::Unspecified, rules::flake8_unused_arguments::rules::UnusedFunctionArgument),
|
||||
@@ -682,6 +685,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8PytestStyle, "011") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestRaisesTooBroad),
|
||||
(Flake8PytestStyle, "012") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestRaisesWithMultipleStatements),
|
||||
(Flake8PytestStyle, "013") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestIncorrectPytestImport),
|
||||
(Flake8PytestStyle, "014") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestDuplicateParametrizeTestCases),
|
||||
(Flake8PytestStyle, "015") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestAssertAlwaysFalse),
|
||||
(Flake8PytestStyle, "016") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestFailWithoutMessage),
|
||||
(Flake8PytestStyle, "017") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestAssertInExcept),
|
||||
@@ -694,6 +698,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8PytestStyle, "024") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUnnecessaryAsyncioMarkOnFixture),
|
||||
(Flake8PytestStyle, "025") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestErroneousUseFixturesOnFixture),
|
||||
(Flake8PytestStyle, "026") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUseFixturesWithoutParameters),
|
||||
(Flake8PytestStyle, "027") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUnittestRaisesAssertion),
|
||||
|
||||
// flake8-pie
|
||||
(Flake8Pie, "790") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryPass),
|
||||
@@ -811,6 +816,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
|
||||
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
|
||||
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),
|
||||
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
|
||||
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use globset::GlobMatcher;
|
||||
use log::debug;
|
||||
use path_absolutize::{path_dedot, Absolutize};
|
||||
use path_absolutize::Absolutize;
|
||||
|
||||
use crate::registry::RuleSet;
|
||||
|
||||
@@ -61,7 +61,13 @@ pub fn normalize_path_to<P: AsRef<Path>, R: AsRef<Path>>(path: P, project_root:
|
||||
/// Convert an absolute path to be relative to the current working directory.
|
||||
pub fn relativize_path<P: AsRef<Path>>(path: P) -> String {
|
||||
let path = path.as_ref();
|
||||
if let Ok(path) = path.strip_prefix(&*path_dedot::CWD) {
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let cwd = Path::new(".");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let cwd = path_absolutize::path_dedot::CWD.as_path();
|
||||
|
||||
if let Ok(path) = path.strip_prefix(cwd) {
|
||||
return format!("{}", path.display());
|
||||
}
|
||||
format!("{}", path.display())
|
||||
|
||||
@@ -67,9 +67,13 @@ impl<'a> Insertion<'a> {
|
||||
TextSize::default()
|
||||
};
|
||||
|
||||
// Skip over commented lines.
|
||||
// Skip over commented lines, with whitespace separation.
|
||||
for line in UniversalNewlineIterator::with_offset(locator.after(location), location) {
|
||||
if line.trim_whitespace_start().starts_with('#') {
|
||||
let trimmed_line = line.trim_whitespace_start();
|
||||
if trimmed_line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if trimmed_line.starts_with('#') {
|
||||
location = line.full_end();
|
||||
} else {
|
||||
break;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Display;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::iter;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -26,7 +26,7 @@ pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb";
|
||||
|
||||
/// Run round-trip source code generation on a given Jupyter notebook file path.
|
||||
pub fn round_trip(path: &Path) -> anyhow::Result<String> {
|
||||
let mut notebook = Notebook::read(path).map_err(|err| {
|
||||
let mut notebook = Notebook::from_path(path).map_err(|err| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to read notebook file `{}`: {:?}",
|
||||
path.display(),
|
||||
@@ -120,18 +120,30 @@ pub struct Notebook {
|
||||
|
||||
impl Notebook {
|
||||
/// Read the Jupyter Notebook from the given [`Path`].
|
||||
///
|
||||
/// See also the black implementation
|
||||
/// <https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#L1017-L1046>
|
||||
pub fn read(path: &Path) -> Result<Self, Box<Diagnostic>> {
|
||||
let mut reader = BufReader::new(File::open(path).map_err(|err| {
|
||||
pub fn from_path(path: &Path) -> Result<Self, Box<Diagnostic>> {
|
||||
Self::from_reader(BufReader::new(File::open(path).map_err(|err| {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
})?);
|
||||
})?))
|
||||
}
|
||||
|
||||
/// Read the Jupyter Notebook from its JSON string.
|
||||
pub fn from_contents(contents: &str) -> Result<Self, Box<Diagnostic>> {
|
||||
Self::from_reader(Cursor::new(contents))
|
||||
}
|
||||
|
||||
/// Read a Jupyter Notebook from a [`Read`] implementor.
|
||||
///
|
||||
/// See also the black implementation
|
||||
/// <https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#L1017-L1046>
|
||||
fn from_reader<R>(mut reader: R) -> Result<Self, Box<Diagnostic>>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let trailing_newline = reader.seek(SeekFrom::End(-1)).is_ok_and(|_| {
|
||||
let mut buf = [0; 1];
|
||||
reader.read_exact(&mut buf).is_ok_and(|_| buf[0] == b'\n')
|
||||
@@ -144,7 +156,7 @@ impl Notebook {
|
||||
TextRange::default(),
|
||||
)
|
||||
})?;
|
||||
let raw_notebook: RawNotebook = match serde_json::from_reader(reader) {
|
||||
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
|
||||
Ok(notebook) => notebook,
|
||||
Err(err) => {
|
||||
// Translate the error into a diagnostic
|
||||
@@ -159,14 +171,19 @@ impl Notebook {
|
||||
Category::Syntax | Category::Eof => {
|
||||
// Maybe someone saved the python sources (those with the `# %%` separator)
|
||||
// as jupyter notebook instead. Let's help them.
|
||||
let contents = std::fs::read_to_string(path).map_err(|err| {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
})?;
|
||||
let mut contents = String::new();
|
||||
reader
|
||||
.rewind()
|
||||
.and_then(|_| reader.read_to_string(&mut contents))
|
||||
.map_err(|err| {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Check if tokenizing was successful and the file is non-empty
|
||||
if lex(&contents, Mode::Module).any(|result| result.is_err()) {
|
||||
Diagnostic::new(
|
||||
@@ -182,7 +199,7 @@ impl Notebook {
|
||||
Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: format!(
|
||||
"Expected a Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT} extension), \
|
||||
"Expected a Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT}), \
|
||||
which must be internally stored as JSON, \
|
||||
but found a Python source file: {err}"
|
||||
),
|
||||
@@ -484,22 +501,22 @@ mod tests {
|
||||
fn test_invalid() {
|
||||
let path = Path::new("resources/test/fixtures/jupyter/invalid_extension.ipynb");
|
||||
assert_eq!(
|
||||
Notebook::read(path).unwrap_err().kind.body,
|
||||
"SyntaxError: Expected a Jupyter Notebook (.ipynb extension), \
|
||||
Notebook::from_path(path).unwrap_err().kind.body,
|
||||
"SyntaxError: Expected a Jupyter Notebook (.ipynb), \
|
||||
which must be internally stored as JSON, \
|
||||
but found a Python source file: \
|
||||
expected value at line 1 column 1"
|
||||
);
|
||||
let path = Path::new("resources/test/fixtures/jupyter/not_json.ipynb");
|
||||
assert_eq!(
|
||||
Notebook::read(path).unwrap_err().kind.body,
|
||||
Notebook::from_path(path).unwrap_err().kind.body,
|
||||
"SyntaxError: A Jupyter Notebook (.ipynb) must internally be JSON, \
|
||||
but this file isn't valid JSON: \
|
||||
expected value at line 1 column 1"
|
||||
);
|
||||
let path = Path::new("resources/test/fixtures/jupyter/wrong_schema.ipynb");
|
||||
assert_eq!(
|
||||
Notebook::read(path).unwrap_err().kind.body,
|
||||
Notebook::from_path(path).unwrap_err().kind.body,
|
||||
"SyntaxError: This file does not match the schema expected of Jupyter Notebooks: \
|
||||
missing field `cells` at line 1 column 2"
|
||||
);
|
||||
|
||||
@@ -59,7 +59,7 @@ impl Eq for LineWidth {}
|
||||
|
||||
impl PartialOrd for LineWidth {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.width.partial_cmp(&other.width)
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,10 +145,10 @@ impl PartialOrd<LineLength> for LineWidth {
|
||||
/// The size of a tab.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct TabSize(pub NonZeroU8);
|
||||
pub struct TabSize(NonZeroU8);
|
||||
|
||||
impl TabSize {
|
||||
fn as_usize(self) -> usize {
|
||||
pub(crate) fn as_usize(self) -> usize {
|
||||
self.0.get() as usize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ impl RuleSet {
|
||||
/// let set_1 = RuleSet::from_rules(&[Rule::AmbiguousFunctionName, Rule::AnyType]);
|
||||
/// let set_2 = RuleSet::from_rules(&[
|
||||
/// Rule::BadQuotesInlineString,
|
||||
/// Rule::BooleanPositionalValueInFunctionCall,
|
||||
/// Rule::BooleanPositionalValueInCall,
|
||||
/// ]);
|
||||
///
|
||||
/// let union = set_1.union(&set_2);
|
||||
@@ -80,7 +80,7 @@ impl RuleSet {
|
||||
/// assert!(union.contains(Rule::AmbiguousFunctionName));
|
||||
/// assert!(union.contains(Rule::AnyType));
|
||||
/// assert!(union.contains(Rule::BadQuotesInlineString));
|
||||
/// assert!(union.contains(Rule::BooleanPositionalValueInFunctionCall));
|
||||
/// assert!(union.contains(Rule::BooleanPositionalValueInCall));
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn union(mut self, other: &Self) -> Self {
|
||||
@@ -132,7 +132,7 @@ impl RuleSet {
|
||||
/// ])));
|
||||
///
|
||||
/// assert!(!set_1.intersects(&RuleSet::from_rules(&[
|
||||
/// Rule::BooleanPositionalValueInFunctionCall,
|
||||
/// Rule::BooleanPositionalValueInCall,
|
||||
/// Rule::BadQuotesInlineString
|
||||
/// ])));
|
||||
/// ```
|
||||
|
||||
@@ -80,7 +80,7 @@ pub(crate) fn variable_name_task_id(
|
||||
// If the keyword argument is not a string, we can't do anything.
|
||||
let task_id = match &keyword.value {
|
||||
Expr::Constant(constant) => match &constant.value {
|
||||
Constant::Str(value) => value,
|
||||
Constant::Str(ast::StringConstant { value, .. }) => value,
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
|
||||
@@ -76,7 +76,7 @@ impl Violation for SuspiciousPickleUsage {
|
||||
/// import marshal
|
||||
///
|
||||
/// with open("foo.marshal", "rb") as file:
|
||||
/// foo = pickle.load(file)
|
||||
/// foo = marshal.load(file)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use ruff_python_ast::{self as ast, Constant, Expr};
|
||||
|
||||
/// Returns `true` if a function call is allowed to use a boolean trap.
|
||||
pub(super) fn is_allowed_func_call(name: &str) -> bool {
|
||||
@@ -62,18 +58,13 @@ pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
const fn is_boolean_arg(arg: &Expr) -> bool {
|
||||
/// Returns `true` if an expression is a boolean literal.
|
||||
pub(super) const fn is_boolean(expr: &Expr) -> bool {
|
||||
matches!(
|
||||
&arg,
|
||||
&expr,
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Bool(_),
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn add_if_boolean(checker: &mut Checker, arg: &Expr, kind: DiagnosticKind) {
|
||||
if is_boolean_arg(arg) {
|
||||
checker.diagnostics.push(Diagnostic::new(kind, arg.range()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ mod tests {
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
#[test_case(Rule::BooleanPositionalArgInFunctionDefinition, Path::new("FBT.py"))]
|
||||
#[test_case(Rule::BooleanDefaultValueInFunctionDefinition, Path::new("FBT.py"))]
|
||||
#[test_case(Rule::BooleanPositionalValueInFunctionCall, Path::new("FBT.py"))]
|
||||
#[test_case(Rule::BooleanTypeHintPositionalArgument, Path::new("FBT.py"))]
|
||||
#[test_case(Rule::BooleanDefaultValuePositionalArgument, Path::new("FBT.py"))]
|
||||
#[test_case(Rule::BooleanPositionalValueInCall, Path::new("FBT.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::collect_call_path;
|
||||
use ruff_python_ast::{Decorator, ParameterWithDefault, Parameters, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_boolean_trap::helpers::{is_allowed_func_def, is_boolean};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of boolean positional arguments in function definitions,
|
||||
/// as determined by the presence of a boolean default value.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean positional arguments is confusing as the
|
||||
/// meaning of the boolean value is not clear to the caller and to future
|
||||
/// readers of the code.
|
||||
///
|
||||
/// The use of a boolean will also limit the function to only two possible
|
||||
/// behaviors, which makes the function difficult to extend in the future.
|
||||
///
|
||||
/// Instead, consider refactoring into separate implementations for the
|
||||
/// `True` and `False` cases, using an `Enum`, or making the argument a
|
||||
/// keyword-only argument, to force callers to be explicit when providing
|
||||
/// the argument.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number, up=True):
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5, True) # What does `True` mean?
|
||||
/// round_number(1.5, False) # What does `False` mean?
|
||||
/// ```
|
||||
///
|
||||
/// Instead, refactor into separate implementations:
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_up(number):
|
||||
/// return ceil(number)
|
||||
///
|
||||
///
|
||||
/// def round_down(number):
|
||||
/// return floor(number)
|
||||
///
|
||||
///
|
||||
/// round_up(1.5)
|
||||
/// round_down(1.5)
|
||||
/// ```
|
||||
///
|
||||
/// Or, refactor to use an `Enum`:
|
||||
/// ```python
|
||||
/// from enum import Enum
|
||||
///
|
||||
///
|
||||
/// class RoundingMethod(Enum):
|
||||
/// UP = 1
|
||||
/// DOWN = 2
|
||||
///
|
||||
///
|
||||
/// def round_number(value, method):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Or, make the argument a keyword-only argument:
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number, *, up=True):
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5, up=True)
|
||||
/// round_number(1.5, up=False)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
#[violation]
|
||||
pub struct BooleanDefaultValuePositionalArgument;
|
||||
|
||||
impl Violation for BooleanDefaultValuePositionalArgument {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Boolean default positional argument in function definition")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn boolean_default_value_positional_argument(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
decorator_list: &[Decorator],
|
||||
parameters: &Parameters,
|
||||
) {
|
||||
if is_allowed_func_def(name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if decorator_list.iter().any(|decorator| {
|
||||
collect_call_path(&decorator.expression)
|
||||
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
for ParameterWithDefault {
|
||||
parameter,
|
||||
default,
|
||||
range: _,
|
||||
} in parameters.posonlyargs.iter().chain(¶meters.args)
|
||||
{
|
||||
if default.as_ref().is_some_and(|default| is_boolean(default)) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BooleanDefaultValuePositionalArgument,
|
||||
parameter.name.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
use ruff_python_ast::Expr;
|
||||
|
||||
use ruff_diagnostics::Violation;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, allow_boolean_trap};
|
||||
use crate::rules::flake8_boolean_trap::helpers::{allow_boolean_trap, is_boolean};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for boolean positional arguments in function calls.
|
||||
@@ -17,44 +15,42 @@ use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, allow_boolean_t
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def foo(flag: bool) -> None:
|
||||
/// def func(flag: bool) -> None:
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// foo(True)
|
||||
/// func(True)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo(flag: bool) -> None:
|
||||
/// def func(flag: bool) -> None:
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// foo(flag=True)
|
||||
/// func(flag=True)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
#[violation]
|
||||
pub struct BooleanPositionalValueInFunctionCall;
|
||||
pub struct BooleanPositionalValueInCall;
|
||||
|
||||
impl Violation for BooleanPositionalValueInFunctionCall {
|
||||
impl Violation for BooleanPositionalValueInCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Boolean positional value in function call")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_boolean_positional_value_in_function_call(
|
||||
checker: &mut Checker,
|
||||
args: &[Expr],
|
||||
func: &Expr,
|
||||
) {
|
||||
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, args: &[Expr], func: &Expr) {
|
||||
if allow_boolean_trap(func) {
|
||||
return;
|
||||
}
|
||||
for arg in args {
|
||||
add_if_boolean(checker, arg, BooleanPositionalValueInFunctionCall.into());
|
||||
for arg in args.iter().filter(|arg| is_boolean(arg)) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(BooleanPositionalValueInCall, arg.range()));
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,22 @@ use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for boolean positional arguments in function definitions.
|
||||
/// Checks for the use of boolean positional arguments in function definitions,
|
||||
/// as determined by the presence of a `bool` type hint.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean positional arguments is confusing as the
|
||||
/// meaning of the boolean value is not clear to the caller, and to future
|
||||
/// meaning of the boolean value is not clear to the caller and to future
|
||||
/// readers of the code.
|
||||
///
|
||||
/// The use of a boolean will also limit the function to only two possible
|
||||
/// behaviors, which makes the function difficult to extend in the future.
|
||||
///
|
||||
/// Instead, consider refactoring into separate implementations for the
|
||||
/// `True` and `False` cases, using an `Enum`, or making the argument a
|
||||
/// keyword-only argument, to force callers to be explicit when providing
|
||||
/// the argument.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
@@ -65,20 +71,33 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Or, make the argument a keyword-only argument:
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number: float, *, up: bool) -> int:
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5, up=True)
|
||||
/// round_number(1.5, up=False)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
#[violation]
|
||||
pub struct BooleanPositionalArgInFunctionDefinition;
|
||||
pub struct BooleanTypeHintPositionalArgument;
|
||||
|
||||
impl Violation for BooleanPositionalArgInFunctionDefinition {
|
||||
impl Violation for BooleanTypeHintPositionalArgument {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Boolean positional arg in function definition")
|
||||
format!("Boolean-typed positional argument in function definition")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_positional_boolean_in_def(
|
||||
pub(crate) fn boolean_type_hint_positional_argument(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
decorator_list: &[Decorator],
|
||||
@@ -101,28 +120,25 @@ pub(crate) fn check_positional_boolean_in_def(
|
||||
range: _,
|
||||
} in parameters.posonlyargs.iter().chain(¶meters.args)
|
||||
{
|
||||
if parameter.annotation.is_none() {
|
||||
continue;
|
||||
}
|
||||
let Some(expr) = ¶meter.annotation else {
|
||||
let Some(annotation) = parameter.annotation.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// check for both bool (python class) and 'bool' (string annotation)
|
||||
let hint = match expr.as_ref() {
|
||||
let hint = match annotation.as_ref() {
|
||||
Expr::Name(name) => &name.id == "bool",
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
value: Constant::Str(ast::StringConstant { value, .. }),
|
||||
..
|
||||
}) => value == "bool",
|
||||
_ => false,
|
||||
};
|
||||
if !hint {
|
||||
if !hint || !checker.semantic().is_builtin("bool") {
|
||||
continue;
|
||||
}
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BooleanPositionalArgInFunctionDefinition,
|
||||
parameter.range(),
|
||||
BooleanTypeHintPositionalArgument,
|
||||
parameter.name.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
use ruff_python_ast::{Decorator, ParameterWithDefault, Parameters};
|
||||
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::collect_call_path;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, is_allowed_func_def};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of booleans as default values in function definitions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean default means that the keyword argument
|
||||
/// argument can be omitted, which makes the function call ambiguous.
|
||||
///
|
||||
/// Instead, define the relevant argument as keyword-only.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number: float, *, up: bool = True) -> int:
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5)
|
||||
/// round_number(1.5, up=False)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
///
|
||||
/// def round_number(number: float, *, up: bool) -> int:
|
||||
/// return ceil(number) if up else floor(number)
|
||||
///
|
||||
///
|
||||
/// round_number(1.5, up=True)
|
||||
/// round_number(1.5, up=False)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
#[violation]
|
||||
pub struct BooleanDefaultValueInFunctionDefinition;
|
||||
|
||||
impl Violation for BooleanDefaultValueInFunctionDefinition {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Boolean default value in function definition")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_boolean_default_value_in_function_definition(
|
||||
checker: &mut Checker,
|
||||
name: &str,
|
||||
decorator_list: &[Decorator],
|
||||
parameters: &Parameters,
|
||||
) {
|
||||
if is_allowed_func_def(name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if decorator_list.iter().any(|decorator| {
|
||||
collect_call_path(&decorator.expression)
|
||||
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
for ParameterWithDefault {
|
||||
parameter: _,
|
||||
default,
|
||||
range: _,
|
||||
} in parameters.args.iter().chain(¶meters.posonlyargs)
|
||||
{
|
||||
let Some(default) = default else {
|
||||
continue;
|
||||
};
|
||||
add_if_boolean(
|
||||
checker,
|
||||
default,
|
||||
BooleanDefaultValueInFunctionDefinition.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
pub(crate) use check_boolean_default_value_in_function_definition::*;
|
||||
pub(crate) use check_boolean_positional_value_in_function_call::*;
|
||||
pub(crate) use check_positional_boolean_in_def::*;
|
||||
pub(crate) use boolean_default_value_positional_argument::*;
|
||||
pub(crate) use boolean_positional_value_in_call::*;
|
||||
pub(crate) use boolean_type_hint_positional_argument::*;
|
||||
|
||||
mod check_boolean_default_value_in_function_definition;
|
||||
mod check_boolean_positional_value_in_function_call;
|
||||
mod check_positional_boolean_in_def;
|
||||
mod boolean_default_value_positional_argument;
|
||||
mod boolean_positional_value_in_call;
|
||||
mod boolean_type_hint_positional_argument;
|
||||
|
||||
@@ -1,91 +1,91 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_boolean_trap/mod.rs
|
||||
---
|
||||
FBT.py:4:5: FBT001 Boolean positional arg in function definition
|
||||
FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
2 | posonly_nohint,
|
||||
3 | posonly_nonboolhint: int,
|
||||
4 | posonly_boolhint: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
| ^^^^^^^^^^^^^^^^ FBT001
|
||||
5 | posonly_boolstrhint: "bool",
|
||||
6 | /,
|
||||
|
|
||||
|
||||
FBT.py:5:5: FBT001 Boolean positional arg in function definition
|
||||
FBT.py:5:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
3 | posonly_nonboolhint: int,
|
||||
4 | posonly_boolhint: bool,
|
||||
5 | posonly_boolstrhint: "bool",
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
| ^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
6 | /,
|
||||
7 | offset,
|
||||
|
|
||||
|
||||
FBT.py:10:5: FBT001 Boolean positional arg in function definition
|
||||
FBT.py:10:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
8 | posorkw_nonvalued_nohint,
|
||||
9 | posorkw_nonvalued_nonboolhint: int,
|
||||
10 | posorkw_nonvalued_boolhint: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
11 | posorkw_nonvalued_boolstrhint: "bool",
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
|
|
||||
|
||||
FBT.py:11:5: FBT001 Boolean positional arg in function definition
|
||||
FBT.py:11:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
9 | posorkw_nonvalued_nonboolhint: int,
|
||||
10 | posorkw_nonvalued_boolhint: bool,
|
||||
11 | posorkw_nonvalued_boolstrhint: "bool",
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
|
|
||||
|
||||
FBT.py:14:5: FBT001 Boolean positional arg in function definition
|
||||
FBT.py:14:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
14 | posorkw_boolvalued_boolhint: bool = True,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
|
|
||||
|
||||
FBT.py:15:5: FBT001 Boolean positional arg in function definition
|
||||
FBT.py:15:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
14 | posorkw_boolvalued_boolhint: bool = True,
|
||||
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
||||
|
|
||||
|
||||
FBT.py:18:5: FBT001 Boolean positional arg in function definition
|
||||
FBT.py:18:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
||||
18 | posorkw_nonboolvalued_boolhint: bool = 3,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
|
||||
20 | *,
|
||||
|
|
||||
|
||||
FBT.py:19:5: FBT001 Boolean positional arg in function definition
|
||||
FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
||||
18 | posorkw_nonboolvalued_boolhint: bool = 3,
|
||||
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
20 | *,
|
||||
21 | kwonly_nonvalued_nohint,
|
||||
|
|
||||
|
||||
FBT.py:86:19: FBT001 Boolean positional arg in function definition
|
||||
FBT.py:86:19: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
85 | # FBT001: Boolean positional arg in function definition
|
||||
86 | def foo(self, value: bool) -> None:
|
||||
| ^^^^^^^^^^^ FBT001
|
||||
| ^^^^^ FBT001
|
||||
87 | pass
|
||||
|
|
||||
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_boolean_trap/mod.rs
|
||||
---
|
||||
FBT.py:12:31: FBT002 Boolean default value in function definition
|
||||
FBT.py:12:5: FBT002 Boolean default positional argument in function definition
|
||||
|
|
||||
10 | posorkw_nonvalued_boolhint: bool,
|
||||
11 | posorkw_nonvalued_boolstrhint: "bool",
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
| ^^^^ FBT002
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
14 | posorkw_boolvalued_boolhint: bool = True,
|
||||
|
|
||||
|
||||
FBT.py:13:43: FBT002 Boolean default value in function definition
|
||||
FBT.py:13:5: FBT002 Boolean default positional argument in function definition
|
||||
|
|
||||
11 | posorkw_nonvalued_boolstrhint: "bool",
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
| ^^^^ FBT002
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
|
||||
14 | posorkw_boolvalued_boolhint: bool = True,
|
||||
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
||||
|
|
||||
|
||||
FBT.py:14:41: FBT002 Boolean default value in function definition
|
||||
FBT.py:14:5: FBT002 Boolean default positional argument in function definition
|
||||
|
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
14 | posorkw_boolvalued_boolhint: bool = True,
|
||||
| ^^^^ FBT002
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
|
||||
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
|
|
||||
|
||||
FBT.py:15:46: FBT002 Boolean default value in function definition
|
||||
FBT.py:15:5: FBT002 Boolean default positional argument in function definition
|
||||
|
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
14 | posorkw_boolvalued_boolhint: bool = True,
|
||||
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
||||
| ^^^^ FBT002
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
||||
|
|
||||
|
||||
@@ -64,7 +64,7 @@ pub(crate) fn getattr_with_constant(
|
||||
return;
|
||||
};
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
value: Constant::Str(ast::StringConstant { value, .. }),
|
||||
..
|
||||
}) = arg
|
||||
else {
|
||||
|
||||
@@ -74,7 +74,6 @@ fn walk_stmt(checker: &mut Checker, body: &[Stmt], f: fn(&Stmt) -> bool) {
|
||||
}
|
||||
Stmt::If(ast::StmtIf { body, .. })
|
||||
| Stmt::Try(ast::StmtTry { body, .. })
|
||||
| Stmt::TryStar(ast::StmtTryStar { body, .. })
|
||||
| Stmt::With(ast::StmtWith { body, .. }) => {
|
||||
walk_stmt(checker, body, f);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use ruff_python_ast::{ParameterWithDefault, Parameters, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
use ruff_python_ast::{self as ast, Expr, Parameter, ParameterWithDefault, Ranged};
|
||||
use ruff_python_codegen::{Generator, Stylist};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
|
||||
use ruff_python_trivia::{indentation_at_offset, textwrap};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of mutable objects as function argument defaults.
|
||||
@@ -50,24 +55,30 @@ use crate::checkers::ast::Checker;
|
||||
pub struct MutableArgumentDefault;
|
||||
|
||||
impl Violation for MutableArgumentDefault {
|
||||
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Do not use mutable data structures for argument defaults")
|
||||
}
|
||||
fn autofix_title(&self) -> Option<String> {
|
||||
Some(format!("Replace with `None`; initialize within function"))
|
||||
}
|
||||
}
|
||||
|
||||
/// B006
|
||||
pub(crate) fn mutable_argument_default(checker: &mut Checker, parameters: &Parameters) {
|
||||
pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
|
||||
// Scan in reverse order to right-align zip().
|
||||
for ParameterWithDefault {
|
||||
parameter,
|
||||
default,
|
||||
range: _,
|
||||
} in parameters
|
||||
} in function_def
|
||||
.parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
.chain(&function_def.parameters.args)
|
||||
.chain(&function_def.parameters.kwonlyargs)
|
||||
{
|
||||
let Some(default) = default else {
|
||||
continue;
|
||||
@@ -79,9 +90,84 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, parameters: &Param
|
||||
.as_ref()
|
||||
.is_some_and(|expr| is_immutable_annotation(expr, checker.semantic()))
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(MutableArgumentDefault, default.range()));
|
||||
let mut diagnostic = Diagnostic::new(MutableArgumentDefault, default.range());
|
||||
|
||||
// If the function body is on the same line as the function def, do not fix
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = move_initialization(
|
||||
function_def,
|
||||
parameter,
|
||||
default,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.indexer(),
|
||||
checker.generator(),
|
||||
) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a [`Fix`] to move a mutable argument default initialization
|
||||
/// into the function body.
|
||||
fn move_initialization(
|
||||
function_def: &ast::StmtFunctionDef,
|
||||
parameter: &Parameter,
|
||||
default: &Expr,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
indexer: &Indexer,
|
||||
generator: Generator,
|
||||
) -> Option<Fix> {
|
||||
let mut body = function_def.body.iter();
|
||||
|
||||
let statement = body.next()?;
|
||||
if indexer.preceded_by_multi_statement_line(statement, locator) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Determine the indentation depth of the function body.
|
||||
let indentation = indentation_at_offset(statement.start(), locator)?;
|
||||
|
||||
// Set the default argument value to `None`.
|
||||
let default_edit = Edit::range_replacement("None".to_string(), default.range());
|
||||
|
||||
// Add an `if`, to set the argument to its original value if still `None`.
|
||||
let mut content = String::new();
|
||||
content.push_str(&format!("if {} is None:", parameter.name.as_str()));
|
||||
content.push_str(stylist.line_ending().as_str());
|
||||
content.push_str(stylist.indentation());
|
||||
content.push_str(&format!(
|
||||
"{} = {}",
|
||||
parameter.name.as_str(),
|
||||
generator.expr(default)
|
||||
));
|
||||
content.push_str(stylist.line_ending().as_str());
|
||||
|
||||
// Indent the edit to match the body indentation.
|
||||
let content = textwrap::indent(&content, indentation).to_string();
|
||||
|
||||
let initialization_edit = if is_docstring_stmt(statement) {
|
||||
// If the first statement in the function is a docstring, insert _after_ it.
|
||||
if let Some(statement) = body.next() {
|
||||
// If there's a second statement, insert _before_ it, but ensure this isn't a
|
||||
// multi-statement line.
|
||||
if indexer.preceded_by_multi_statement_line(statement, locator) {
|
||||
return None;
|
||||
}
|
||||
Edit::insertion(content, locator.line_start(statement.start()))
|
||||
} else {
|
||||
// If the docstring is the only statement, insert _before_ it.
|
||||
Edit::insertion(content, locator.full_line_end(statement.end()))
|
||||
}
|
||||
} else {
|
||||
// Otherwise, insert before the first statement.
|
||||
let at = locator.line_start(statement.start());
|
||||
Edit::insertion(content, at)
|
||||
};
|
||||
|
||||
Some(Fix::manual_edits(default_edit, [initialization_edit]))
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ pub(crate) fn setattr_with_constant(
|
||||
if !is_identifier(name) {
|
||||
return;
|
||||
}
|
||||
if is_mangled_private(name.as_str()) {
|
||||
if is_mangled_private(name) {
|
||||
return;
|
||||
}
|
||||
// We can only replace a `setattr` call (which is an `Expr`) with an assignment
|
||||
|
||||
@@ -68,13 +68,13 @@ pub(crate) fn unreliable_callable_check(
|
||||
return;
|
||||
};
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(string),
|
||||
value: Constant::Str(ast::StringConstant { value, .. }),
|
||||
..
|
||||
}) = attr
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if string != "__call__" {
|
||||
if value != "__call__" {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,123 +1,479 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_B008.py:63:25: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:63:25: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
63 | def this_is_wrong(value=[1, 2, 3]):
|
||||
| ^^^^^^^^^ B006
|
||||
64 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:67:30: B006 Do not use mutable data structures for argument defaults
|
||||
ℹ Possible fix
|
||||
60 60 | # Flag mutable literals/comprehensions
|
||||
61 61 |
|
||||
62 62 |
|
||||
63 |-def this_is_wrong(value=[1, 2, 3]):
|
||||
63 |+def this_is_wrong(value=None):
|
||||
64 |+ if value is None:
|
||||
65 |+ value = [1, 2, 3]
|
||||
64 66 | ...
|
||||
65 67 |
|
||||
66 68 |
|
||||
|
||||
B006_B008.py:67:30: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
67 | def this_is_also_wrong(value={}):
|
||||
| ^^ B006
|
||||
68 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:71:20: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
71 | def and_this(value=set()):
|
||||
| ^^^^^ B006
|
||||
72 | ...
|
||||
|
|
||||
ℹ Possible fix
|
||||
64 64 | ...
|
||||
65 65 |
|
||||
66 66 |
|
||||
67 |-def this_is_also_wrong(value={}):
|
||||
67 |+def this_is_also_wrong(value=None):
|
||||
68 |+ if value is None:
|
||||
69 |+ value = {}
|
||||
68 70 | ...
|
||||
69 71 |
|
||||
70 72 |
|
||||
|
||||
B006_B008.py:75:20: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:73:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
75 | def this_too(value=collections.OrderedDict()):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
76 | ...
|
||||
71 | class Foo:
|
||||
72 | @staticmethod
|
||||
73 | def this_is_also_wrong_and_more_indented(value={}):
|
||||
| ^^ B006
|
||||
74 | pass
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:79:32: B006 Do not use mutable data structures for argument defaults
|
||||
ℹ Possible fix
|
||||
70 70 |
|
||||
71 71 | class Foo:
|
||||
72 72 | @staticmethod
|
||||
73 |- def this_is_also_wrong_and_more_indented(value={}):
|
||||
73 |+ def this_is_also_wrong_and_more_indented(value=None):
|
||||
74 |+ if value is None:
|
||||
75 |+ value = {}
|
||||
74 76 | pass
|
||||
75 77 |
|
||||
76 78 |
|
||||
|
||||
B006_B008.py:77:31: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
77 | def multiline_arg_wrong(value={
|
||||
| _______________________________^
|
||||
78 | |
|
||||
79 | | }):
|
||||
| |_^ B006
|
||||
80 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
74 74 | pass
|
||||
75 75 |
|
||||
76 76 |
|
||||
77 |-def multiline_arg_wrong(value={
|
||||
78 |-
|
||||
79 |-}):
|
||||
77 |+def multiline_arg_wrong(value=None):
|
||||
78 |+ if value is None:
|
||||
79 |+ value = {}
|
||||
80 80 | ...
|
||||
81 81 |
|
||||
82 82 | def single_line_func_wrong(value = {}): ...
|
||||
|
||||
B006_B008.py:82:36: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
79 | async def async_this_too(value=collections.defaultdict()):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
80 | ...
|
||||
81 |
|
||||
82 | def single_line_func_wrong(value = {}): ...
|
||||
| ^^ B006
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:83:26: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:85:20: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
83 | def dont_forget_me(value=collections.deque()):
|
||||
85 | def and_this(value=set()):
|
||||
| ^^^^^ B006
|
||||
86 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
82 82 | def single_line_func_wrong(value = {}): ...
|
||||
83 83 |
|
||||
84 84 |
|
||||
85 |-def and_this(value=set()):
|
||||
85 |+def and_this(value=None):
|
||||
86 |+ if value is None:
|
||||
87 |+ value = set()
|
||||
86 88 | ...
|
||||
87 89 |
|
||||
88 90 |
|
||||
|
||||
B006_B008.py:89:20: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
89 | def this_too(value=collections.OrderedDict()):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
90 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
86 86 | ...
|
||||
87 87 |
|
||||
88 88 |
|
||||
89 |-def this_too(value=collections.OrderedDict()):
|
||||
89 |+def this_too(value=None):
|
||||
90 |+ if value is None:
|
||||
91 |+ value = collections.OrderedDict()
|
||||
90 92 | ...
|
||||
91 93 |
|
||||
92 94 |
|
||||
|
||||
B006_B008.py:93:32: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
93 | async def async_this_too(value=collections.defaultdict()):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
94 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
90 90 | ...
|
||||
91 91 |
|
||||
92 92 |
|
||||
93 |-async def async_this_too(value=collections.defaultdict()):
|
||||
93 |+async def async_this_too(value=None):
|
||||
94 |+ if value is None:
|
||||
95 |+ value = collections.defaultdict()
|
||||
94 96 | ...
|
||||
95 97 |
|
||||
96 98 |
|
||||
|
||||
B006_B008.py:97:26: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
97 | def dont_forget_me(value=collections.deque()):
|
||||
| ^^^^^^^^^^^^^^^^^^^ B006
|
||||
84 | ...
|
||||
98 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:88:46: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
87 | # N.B. we're also flagging the function call in the comprehension
|
||||
88 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
89 | pass
|
||||
|
|
||||
ℹ Possible fix
|
||||
94 94 | ...
|
||||
95 95 |
|
||||
96 96 |
|
||||
97 |-def dont_forget_me(value=collections.deque()):
|
||||
97 |+def dont_forget_me(value=None):
|
||||
98 |+ if value is None:
|
||||
99 |+ value = collections.deque()
|
||||
98 100 | ...
|
||||
99 101 |
|
||||
100 102 |
|
||||
|
||||
B006_B008.py:92:46: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
92 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
93 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:96:45: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
96 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
97 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:100:33: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:102:46: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
100 | def kwonlyargs_mutable(*, value=[]):
|
||||
101 | # N.B. we're also flagging the function call in the comprehension
|
||||
102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
103 | pass
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
99 99 |
|
||||
100 100 |
|
||||
101 101 | # N.B. we're also flagging the function call in the comprehension
|
||||
102 |-def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
|
||||
102 |+def list_comprehension_also_not_okay(default=None):
|
||||
103 |+ if default is None:
|
||||
104 |+ default = [i ** 2 for i in range(3)]
|
||||
103 105 | pass
|
||||
104 106 |
|
||||
105 107 |
|
||||
|
||||
B006_B008.py:106:46: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
107 | pass
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
103 103 | pass
|
||||
104 104 |
|
||||
105 105 |
|
||||
106 |-def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
|
||||
106 |+def dict_comprehension_also_not_okay(default=None):
|
||||
107 |+ if default is None:
|
||||
108 |+ default = {i: i ** 2 for i in range(3)}
|
||||
107 109 | pass
|
||||
108 110 |
|
||||
109 111 |
|
||||
|
||||
B006_B008.py:110:45: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
111 | pass
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
107 107 | pass
|
||||
108 108 |
|
||||
109 109 |
|
||||
110 |-def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
|
||||
110 |+def set_comprehension_also_not_okay(default=None):
|
||||
111 |+ if default is None:
|
||||
112 |+ default = {i ** 2 for i in range(3)}
|
||||
111 113 | pass
|
||||
112 114 |
|
||||
113 115 |
|
||||
|
||||
B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
114 | def kwonlyargs_mutable(*, value=[]):
|
||||
| ^^ B006
|
||||
101 | ...
|
||||
115 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:221:20: B006 Do not use mutable data structures for argument defaults
|
||||
ℹ Possible fix
|
||||
111 111 | pass
|
||||
112 112 |
|
||||
113 113 |
|
||||
114 |-def kwonlyargs_mutable(*, value=[]):
|
||||
114 |+def kwonlyargs_mutable(*, value=None):
|
||||
115 |+ if value is None:
|
||||
116 |+ value = []
|
||||
115 117 | ...
|
||||
116 118 |
|
||||
117 119 |
|
||||
|
||||
B006_B008.py:235:20: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
219 | # B006 and B008
|
||||
220 | # We should handle arbitrary nesting of these B008.
|
||||
221 | def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
233 | # B006 and B008
|
||||
234 | # We should handle arbitrary nesting of these B008.
|
||||
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
222 | pass
|
||||
236 | pass
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:258:27: B006 Do not use mutable data structures for argument defaults
|
||||
ℹ Possible fix
|
||||
232 232 |
|
||||
233 233 | # B006 and B008
|
||||
234 234 | # We should handle arbitrary nesting of these B008.
|
||||
235 |-def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
235 |+def nested_combo(a=None):
|
||||
236 |+ if a is None:
|
||||
237 |+ a = [float(3), dt.datetime.now()]
|
||||
236 238 | pass
|
||||
237 239 |
|
||||
238 240 |
|
||||
|
||||
B006_B008.py:272:27: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
257 | def mutable_annotations(
|
||||
258 | a: list[int] | None = [],
|
||||
271 | def mutable_annotations(
|
||||
272 | a: list[int] | None = [],
|
||||
| ^^ B006
|
||||
259 | b: Optional[Dict[int, int]] = {},
|
||||
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
273 | b: Optional[Dict[int, int]] = {},
|
||||
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:259:35: B006 Do not use mutable data structures for argument defaults
|
||||
ℹ Possible fix
|
||||
269 269 |
|
||||
270 270 |
|
||||
271 271 | def mutable_annotations(
|
||||
272 |- a: list[int] | None = [],
|
||||
272 |+ a: list[int] | None = None,
|
||||
273 273 | b: Optional[Dict[int, int]] = {},
|
||||
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
276 276 | ):
|
||||
277 |+ if a is None:
|
||||
278 |+ a = []
|
||||
277 279 | pass
|
||||
278 280 |
|
||||
279 281 |
|
||||
|
||||
B006_B008.py:273:35: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
257 | def mutable_annotations(
|
||||
258 | a: list[int] | None = [],
|
||||
259 | b: Optional[Dict[int, int]] = {},
|
||||
271 | def mutable_annotations(
|
||||
272 | a: list[int] | None = [],
|
||||
273 | b: Optional[Dict[int, int]] = {},
|
||||
| ^^ B006
|
||||
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:260:62: B006 Do not use mutable data structures for argument defaults
|
||||
ℹ Possible fix
|
||||
270 270 |
|
||||
271 271 | def mutable_annotations(
|
||||
272 272 | a: list[int] | None = [],
|
||||
273 |- b: Optional[Dict[int, int]] = {},
|
||||
273 |+ b: Optional[Dict[int, int]] = None,
|
||||
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
276 276 | ):
|
||||
277 |+ if b is None:
|
||||
278 |+ b = {}
|
||||
277 279 | pass
|
||||
278 280 |
|
||||
279 281 |
|
||||
|
||||
B006_B008.py:274:62: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
258 | a: list[int] | None = [],
|
||||
259 | b: Optional[Dict[int, int]] = {},
|
||||
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
272 | a: list[int] | None = [],
|
||||
273 | b: Optional[Dict[int, int]] = {},
|
||||
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
| ^^^^^ B006
|
||||
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
262 | ):
|
||||
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
276 | ):
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:261:80: B006 Do not use mutable data structures for argument defaults
|
||||
ℹ Possible fix
|
||||
271 271 | def mutable_annotations(
|
||||
272 272 | a: list[int] | None = [],
|
||||
273 273 | b: Optional[Dict[int, int]] = {},
|
||||
274 |- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
274 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
|
||||
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
276 276 | ):
|
||||
277 |+ if c is None:
|
||||
278 |+ c = set()
|
||||
277 279 | pass
|
||||
278 280 |
|
||||
279 281 |
|
||||
|
||||
B006_B008.py:275:80: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
259 | b: Optional[Dict[int, int]] = {},
|
||||
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
273 | b: Optional[Dict[int, int]] = {},
|
||||
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
| ^^^^^ B006
|
||||
262 | ):
|
||||
263 | pass
|
||||
276 | ):
|
||||
277 | pass
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
272 272 | a: list[int] | None = [],
|
||||
273 273 | b: Optional[Dict[int, int]] = {},
|
||||
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
|
||||
276 276 | ):
|
||||
277 |+ if d is None:
|
||||
278 |+ d = set()
|
||||
277 279 | pass
|
||||
278 280 |
|
||||
279 281 |
|
||||
|
||||
B006_B008.py:280:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
280 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
281 | """Docstring"""
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
277 277 | pass
|
||||
278 278 |
|
||||
279 279 |
|
||||
280 |-def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
280 |+def single_line_func_wrong(value: dict[str, str] = None):
|
||||
281 281 | """Docstring"""
|
||||
282 |+ if value is None:
|
||||
283 |+ value = {}
|
||||
282 284 |
|
||||
283 285 |
|
||||
284 286 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
284 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
285 | """Docstring"""
|
||||
286 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
281 281 | """Docstring"""
|
||||
282 282 |
|
||||
283 283 |
|
||||
284 |-def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
284 |+def single_line_func_wrong(value: dict[str, str] = None):
|
||||
285 285 | """Docstring"""
|
||||
286 |+ if value is None:
|
||||
287 |+ value = {}
|
||||
286 288 | ...
|
||||
287 289 |
|
||||
288 290 |
|
||||
|
||||
B006_B008.py:289:52: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
289 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
290 | """Docstring"""; ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:293:52: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
293 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
294 | """Docstring"""; \
|
||||
295 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:298:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
298 | def single_line_func_wrong(value: dict[str, str] = {
|
||||
| ____________________________________________________^
|
||||
299 | | # This is a comment
|
||||
300 | | }):
|
||||
| |_^ B006
|
||||
301 | """Docstring"""
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
295 295 | ...
|
||||
296 296 |
|
||||
297 297 |
|
||||
298 |-def single_line_func_wrong(value: dict[str, str] = {
|
||||
299 |- # This is a comment
|
||||
300 |-}):
|
||||
298 |+def single_line_func_wrong(value: dict[str, str] = None):
|
||||
301 299 | """Docstring"""
|
||||
300 |+ if value is None:
|
||||
301 |+ value = {}
|
||||
302 302 |
|
||||
303 303 |
|
||||
304 304 | def single_line_func_wrong(value: dict[str, str] = {}) \
|
||||
|
||||
B006_B008.py:304:52: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
304 | def single_line_func_wrong(value: dict[str, str] = {}) \
|
||||
| ^^ B006
|
||||
305 | : \
|
||||
306 | """Docstring"""
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
|
||||
|
||||
@@ -1,83 +1,83 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B006_B008.py:88:61: B008 Do not perform function call `range` in argument defaults
|
||||
|
|
||||
87 | # N.B. we're also flagging the function call in the comprehension
|
||||
88 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
|
||||
| ^^^^^^^^ B008
|
||||
89 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:92:64: B008 Do not perform function call `range` in argument defaults
|
||||
|
|
||||
92 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
|
||||
| ^^^^^^^^ B008
|
||||
93 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:96:60: B008 Do not perform function call `range` in argument defaults
|
||||
|
|
||||
96 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
|
||||
| ^^^^^^^^ B008
|
||||
97 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:112:39: B008 Do not perform function call `time.time` in argument defaults
|
||||
B006_B008.py:102:61: B008 Do not perform function call `range` in argument defaults
|
||||
|
|
||||
110 | # B008
|
||||
111 | # Flag function calls as default args (including if they are part of a sub-expression)
|
||||
112 | def in_fact_all_calls_are_wrong(value=time.time()):
|
||||
101 | # N.B. we're also flagging the function call in the comprehension
|
||||
102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
|
||||
| ^^^^^^^^ B008
|
||||
103 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:106:64: B008 Do not perform function call `range` in argument defaults
|
||||
|
|
||||
106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
|
||||
| ^^^^^^^^ B008
|
||||
107 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:110:60: B008 Do not perform function call `range` in argument defaults
|
||||
|
|
||||
110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
|
||||
| ^^^^^^^^ B008
|
||||
111 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:126:39: B008 Do not perform function call `time.time` in argument defaults
|
||||
|
|
||||
124 | # B008
|
||||
125 | # Flag function calls as default args (including if they are part of a sub-expression)
|
||||
126 | def in_fact_all_calls_are_wrong(value=time.time()):
|
||||
| ^^^^^^^^^^^ B008
|
||||
113 | ...
|
||||
127 | ...
|
||||
|
|
||||
|
||||
B006_B008.py:116:12: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
||||
B006_B008.py:130:12: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
||||
|
|
||||
116 | def f(when=dt.datetime.now() + dt.timedelta(days=7)):
|
||||
130 | def f(when=dt.datetime.now() + dt.timedelta(days=7)):
|
||||
| ^^^^^^^^^^^^^^^^^ B008
|
||||
117 | pass
|
||||
131 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:120:30: B008 Do not perform function call in argument defaults
|
||||
B006_B008.py:134:30: B008 Do not perform function call in argument defaults
|
||||
|
|
||||
120 | def can_even_catch_lambdas(a=(lambda x: x)()):
|
||||
134 | def can_even_catch_lambdas(a=(lambda x: x)()):
|
||||
| ^^^^^^^^^^^^^^^ B008
|
||||
121 | ...
|
||||
135 | ...
|
||||
|
|
||||
|
||||
B006_B008.py:221:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
||||
B006_B008.py:235:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
||||
|
|
||||
219 | # B006 and B008
|
||||
220 | # We should handle arbitrary nesting of these B008.
|
||||
221 | def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
233 | # B006 and B008
|
||||
234 | # We should handle arbitrary nesting of these B008.
|
||||
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
| ^^^^^^^^^^^^^^^^^ B008
|
||||
222 | pass
|
||||
236 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:227:22: B008 Do not perform function call `map` in argument defaults
|
||||
B006_B008.py:241:22: B008 Do not perform function call `map` in argument defaults
|
||||
|
|
||||
225 | # Don't flag nested B006 since we can't guarantee that
|
||||
226 | # it isn't made mutable by the outer operation.
|
||||
227 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
|
||||
239 | # Don't flag nested B006 since we can't guarantee that
|
||||
240 | # it isn't made mutable by the outer operation.
|
||||
241 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
|
||||
228 | pass
|
||||
242 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:232:19: B008 Do not perform function call `random.randint` in argument defaults
|
||||
B006_B008.py:246:19: B008 Do not perform function call `random.randint` in argument defaults
|
||||
|
|
||||
231 | # B008-ception.
|
||||
232 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
|
||||
245 | # B008-ception.
|
||||
246 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
|
||||
233 | pass
|
||||
247 | pass
|
||||
|
|
||||
|
||||
B006_B008.py:232:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
||||
B006_B008.py:246:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
||||
|
|
||||
231 | # B008-ception.
|
||||
232 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
|
||||
245 | # B008-ception.
|
||||
246 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
|
||||
| ^^^^^^^^^^^^^^^^^ B008
|
||||
233 | pass
|
||||
247 | pass
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
use ruff_python_ast::{self as ast, Expr, Keyword};
|
||||
|
||||
pub(super) fn expr_name(func: &Expr) -> Option<&str> {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = func {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
|
||||
pub(super) fn exactly_one_argument_with_matching_function<'a>(
|
||||
name: &str,
|
||||
@@ -20,7 +12,8 @@ pub(super) fn exactly_one_argument_with_matching_function<'a>(
|
||||
if !keywords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if expr_name(func)? != name {
|
||||
let func = func.as_name_expr()?;
|
||||
if func.id != name {
|
||||
return None;
|
||||
}
|
||||
Some(arg)
|
||||
@@ -31,8 +24,8 @@ pub(super) fn first_argument_with_matching_function<'a>(
|
||||
func: &Expr,
|
||||
args: &'a [Expr],
|
||||
) -> Option<&'a Expr> {
|
||||
if expr_name(func)? == name {
|
||||
Some(args.first()?)
|
||||
if func.as_name_expr().is_some_and(|func| func.id == name) {
|
||||
args.first()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use ruff_python_ast::{self as ast, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary `list` or `reversed` calls around `sorted`
|
||||
/// calls.
|
||||
@@ -57,10 +54,10 @@ pub(crate) fn unnecessary_call_around_sorted(
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) {
|
||||
let Some(outer) = helpers::expr_name(func) else {
|
||||
let Some(outer) = func.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if !(outer == "list" || outer == "reversed") {
|
||||
if !matches!(outer.id.as_str(), "list" | "reversed") {
|
||||
return;
|
||||
}
|
||||
let Some(arg) = args.first() else {
|
||||
@@ -69,18 +66,18 @@ pub(crate) fn unnecessary_call_around_sorted(
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = arg else {
|
||||
return;
|
||||
};
|
||||
let Some(inner) = helpers::expr_name(func) else {
|
||||
let Some(inner) = func.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if inner != "sorted" {
|
||||
if inner.id != "sorted" {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin(inner) || !checker.semantic().is_builtin(outer) {
|
||||
if !checker.semantic().is_builtin(&inner.id) || !checker.semantic().is_builtin(&outer.id) {
|
||||
return;
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryCallAroundSorted {
|
||||
func: outer.to_string(),
|
||||
func: outer.id.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
@@ -91,7 +88,7 @@ pub(crate) fn unnecessary_call_around_sorted(
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)?;
|
||||
if outer == "reversed" {
|
||||
if outer.id == "reversed" {
|
||||
Ok(Fix::suggested(edit))
|
||||
} else {
|
||||
Ok(Fix::automatic(edit))
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
use crate::rules::flake8_comprehensions::settings::Settings;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary `dict`, `list` or `tuple` calls that can be
|
||||
/// rewritten as empty literals.
|
||||
@@ -63,10 +60,10 @@ pub(crate) fn unnecessary_collection_call(
|
||||
if !args.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(id) = helpers::expr_name(func) else {
|
||||
let Some(func) = func.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
match id {
|
||||
match func.id.as_str() {
|
||||
"dict"
|
||||
if keywords.is_empty()
|
||||
|| (!settings.allow_dict_calls_with_keyword_arguments
|
||||
@@ -79,12 +76,12 @@ pub(crate) fn unnecessary_collection_call(
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
if !checker.semantic().is_builtin(id) {
|
||||
if !checker.semantic().is_builtin(func.id.as_str()) {
|
||||
return;
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryCollectionCall {
|
||||
obj_type: id.to_string(),
|
||||
obj_type: func.id.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use ruff_python_ast::{self as ast, Comprehension, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Comprehension, Expr, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary `dict`, `list`, and `set` comprehension.
|
||||
///
|
||||
@@ -88,28 +85,28 @@ pub(crate) fn unnecessary_dict_comprehension(
|
||||
if !generator.ifs.is_empty() || generator.is_async {
|
||||
return;
|
||||
}
|
||||
let Some(key_id) = helpers::expr_name(key) else {
|
||||
let Some(key) = key.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
let Some(value_id) = helpers::expr_name(value) else {
|
||||
let Some(value) = value.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
let Expr::Tuple(ast::ExprTuple { elts, .. }) = &generator.target else {
|
||||
return;
|
||||
};
|
||||
if elts.len() != 2 {
|
||||
return;
|
||||
}
|
||||
let Some(target_key_id) = helpers::expr_name(&elts[0]) else {
|
||||
let [target_key, target_value] = elts.as_slice() else {
|
||||
return;
|
||||
};
|
||||
if target_key_id != key_id {
|
||||
return;
|
||||
}
|
||||
let Some(target_value_id) = helpers::expr_name(&elts[1]) else {
|
||||
let Some(target_key) = target_key.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if target_value_id != value_id {
|
||||
let Some(target_value) = target_value.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if target_key.id != key.id {
|
||||
return;
|
||||
}
|
||||
if target_value.id != value.id {
|
||||
return;
|
||||
}
|
||||
add_diagnostic(checker, expr);
|
||||
@@ -128,13 +125,13 @@ pub(crate) fn unnecessary_list_set_comprehension(
|
||||
if !generator.ifs.is_empty() || generator.is_async {
|
||||
return;
|
||||
}
|
||||
let Some(elt_id) = helpers::expr_name(elt) else {
|
||||
let Some(elt) = elt.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
let Some(target_id) = helpers::expr_name(&generator.target) else {
|
||||
let Some(target) = generator.target.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if elt_id != target_id {
|
||||
if elt.id != target.id {
|
||||
return;
|
||||
}
|
||||
add_diagnostic(checker, expr);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableKeyword;
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary `list`, `reversed`, `set`, `sorted`, and `tuple`
|
||||
/// call within `list`, `set`, `sorted`, and `tuple` calls.
|
||||
@@ -72,15 +69,13 @@ pub(crate) fn unnecessary_double_cast_or_process(
|
||||
args: &[Expr],
|
||||
outer_kw: &[Keyword],
|
||||
) {
|
||||
let Some(outer) = helpers::expr_name(func) else {
|
||||
let Some(outer) = func.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if !(outer == "list"
|
||||
|| outer == "tuple"
|
||||
|| outer == "set"
|
||||
|| outer == "reversed"
|
||||
|| outer == "sorted")
|
||||
{
|
||||
if !matches!(
|
||||
outer.id.as_str(),
|
||||
"list" | "tuple" | "set" | "reversed" | "sorted"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let Some(arg) = args.first() else {
|
||||
@@ -96,16 +91,16 @@ pub(crate) fn unnecessary_double_cast_or_process(
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(inner) = helpers::expr_name(func) else {
|
||||
let Some(inner) = func.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin(inner) || !checker.semantic().is_builtin(outer) {
|
||||
if !checker.semantic().is_builtin(&inner.id) || !checker.semantic().is_builtin(&outer.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid collapsing nested `sorted` calls with non-identical keyword arguments
|
||||
// (i.e., `key`, `reverse`).
|
||||
if inner == "sorted" && outer == "sorted" {
|
||||
if inner.id == "sorted" && outer.id == "sorted" {
|
||||
if inner_kw.len() != outer_kw.len() {
|
||||
return;
|
||||
}
|
||||
@@ -118,18 +113,19 @@ pub(crate) fn unnecessary_double_cast_or_process(
|
||||
}
|
||||
}
|
||||
|
||||
// Ex) set(tuple(...))
|
||||
// Ex) list(tuple(...))
|
||||
// Ex) set(set(...))
|
||||
if ((outer == "set" || outer == "sorted")
|
||||
&& (inner == "list" || inner == "tuple" || inner == "reversed" || inner == "sorted"))
|
||||
|| (outer == "set" && inner == "set")
|
||||
|| ((outer == "list" || outer == "tuple") && (inner == "list" || inner == "tuple"))
|
||||
{
|
||||
// Ex) `set(tuple(...))`
|
||||
// Ex) `list(tuple(...))`
|
||||
// Ex) `set(set(...))`
|
||||
if matches!(
|
||||
(outer.id.as_str(), inner.id.as_str()),
|
||||
("set" | "sorted", "list" | "tuple" | "reversed" | "sorted")
|
||||
| ("set", "set")
|
||||
| ("list" | "tuple", "list" | "tuple")
|
||||
) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryDoubleCastOrProcess {
|
||||
inner: inner.to_string(),
|
||||
outer: outer.to_string(),
|
||||
inner: inner.id.to_string(),
|
||||
outer: outer.id.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
|
||||
@@ -70,7 +70,7 @@ pub(crate) fn unnecessary_literal_dict(
|
||||
// Accept `dict((1, 2), ...))` `dict([(1, 2), ...])`.
|
||||
if !elts
|
||||
.iter()
|
||||
.all(|elt| matches!(&elt, Expr::Tuple(ast::ExprTuple { elts, .. } )if elts.len() == 2))
|
||||
.all(|elt| matches!(&elt, Expr::Tuple(ast::ExprTuple { elts, .. }) if elts.len() == 2))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,11 +68,11 @@ pub(crate) fn unnecessary_map(
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) {
|
||||
let Some(id) = helpers::expr_name(func) else {
|
||||
let Some(func) = func.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let object_type = match id {
|
||||
let object_type = match func.id.as_str() {
|
||||
"map" => ObjectType::Generator,
|
||||
"list" => ObjectType::List,
|
||||
"set" => ObjectType::Set,
|
||||
@@ -80,20 +80,20 @@ pub(crate) fn unnecessary_map(
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if !checker.semantic().is_builtin(id) {
|
||||
if !checker.semantic().is_builtin(&func.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
match object_type {
|
||||
ObjectType::Generator => {
|
||||
// Exclude the parent if already matched by other arms.
|
||||
if let Some(Expr::Call(ast::ExprCall { func, .. })) = parent {
|
||||
if let Some(name) = helpers::expr_name(func) {
|
||||
if matches!(name, "list" | "set" | "dict") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
if parent
|
||||
.and_then(ruff_python_ast::Expr::as_call_expr)
|
||||
.and_then(|call| call.func.as_name_expr())
|
||||
.is_some_and(|name| matches!(name.id.as_str(), "list" | "set" | "dict"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only flag, e.g., `map(lambda x: x + 1, iterable)`.
|
||||
let [Expr::Lambda(ast::ExprLambda {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use num_bigint::BigInt;
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Ranged, UnaryOp};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Ranged, UnaryOp};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary subscript reversal of iterable.
|
||||
///
|
||||
@@ -52,13 +50,13 @@ pub(crate) fn unnecessary_subscript_reversal(
|
||||
let Some(first_arg) = args.first() else {
|
||||
return;
|
||||
};
|
||||
let Some(id) = helpers::expr_name(func) else {
|
||||
let Some(func) = func.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if !(id == "set" || id == "sorted" || id == "reversed") {
|
||||
if !matches!(func.id.as_str(), "reversed" | "set" | "sorted") {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin(id) {
|
||||
if !checker.semantic().is_builtin(&func.id) {
|
||||
return;
|
||||
}
|
||||
let Expr::Subscript(ast::ExprSubscript { slice, .. }) = first_arg else {
|
||||
@@ -99,7 +97,7 @@ pub(crate) fn unnecessary_subscript_reversal(
|
||||
};
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
UnnecessarySubscriptReversal {
|
||||
func: id.to_string(),
|
||||
func: func.id.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
|
||||
@@ -8,6 +8,44 @@ use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for usage of `datetime.datetime.fromtimestamp()` without a `tz`
|
||||
/// argument.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python datetime objects can be naive or timezone-aware. While an aware
|
||||
/// object represents a specific moment in time, a naive object does not
|
||||
/// contain enough information to unambiguously locate itself relative to other
|
||||
/// datetime objects. Since this can lead to errors, it is recommended to
|
||||
/// always use timezone-aware objects.
|
||||
///
|
||||
/// `datetime.datetime.fromtimestamp(ts)` returns a naive datetime object.
|
||||
/// Instead, use `datetime.datetime.fromtimestamp(ts, tz=)` to return a
|
||||
/// timezone-aware object.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import datetime
|
||||
///
|
||||
/// datetime.datetime.fromtimestamp(946684800)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import datetime
|
||||
///
|
||||
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.timezone.utc)
|
||||
/// ```
|
||||
///
|
||||
/// Or, for Python 3.11 and later:
|
||||
/// ```python
|
||||
/// import datetime
|
||||
///
|
||||
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.UTC)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
|
||||
#[violation]
|
||||
pub struct CallDatetimeFromtimestamp;
|
||||
|
||||
@@ -20,7 +58,6 @@ impl Violation for CallDatetimeFromtimestamp {
|
||||
}
|
||||
}
|
||||
|
||||
/// DTZ006
|
||||
pub(crate) fn call_datetime_fromtimestamp(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker
|
||||
.semantic()
|
||||
|
||||
@@ -8,6 +8,42 @@ use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for usage of `datetime.datetime.now()` without a `tz` argument.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python datetime objects can be naive or timezone-aware. While an aware
|
||||
/// object represents a specific moment in time, a naive object does not
|
||||
/// contain enough information to unambiguously locate itself relative to other
|
||||
/// datetime objects. Since this can lead to errors, it is recommended to
|
||||
/// always use timezone-aware objects.
|
||||
///
|
||||
/// `datetime.datetime.now()` returns a naive datetime object. Instead, use
|
||||
/// `datetime.datetime.now(tz=)` to return a timezone-aware object.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import datetime
|
||||
///
|
||||
/// datetime.datetime.now()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import datetime
|
||||
///
|
||||
/// datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
/// ```
|
||||
///
|
||||
/// Or, for Python 3.11 and later:
|
||||
/// ```python
|
||||
/// import datetime
|
||||
///
|
||||
/// datetime.datetime.now(tz=datetime.UTC)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
|
||||
#[violation]
|
||||
pub struct CallDatetimeNowWithoutTzinfo;
|
||||
|
||||
@@ -18,7 +54,6 @@ impl Violation for CallDatetimeNowWithoutTzinfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// DTZ005
|
||||
pub(crate) fn call_datetime_now_without_tzinfo(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker
|
||||
.semantic()
|
||||
|
||||
@@ -83,13 +83,13 @@ pub(crate) fn all_with_model_form(
|
||||
continue;
|
||||
};
|
||||
match value {
|
||||
Constant::Str(s) => {
|
||||
if s == "__all__" {
|
||||
Constant::Str(ast::StringConstant { value, .. }) => {
|
||||
if value == "__all__" {
|
||||
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
|
||||
}
|
||||
}
|
||||
Constant::Bytes(b) => {
|
||||
if b == "__all__".as_bytes() {
|
||||
Constant::Bytes(ast::BytesConstant { value, .. }) => {
|
||||
if value == "__all__".as_bytes() {
|
||||
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ use crate::checkers::ast::Checker;
|
||||
/// annotations at evaluation time, making the code compatible with both past
|
||||
/// and future Python versions.
|
||||
///
|
||||
/// This rule respects the [`target-version`] setting. For example, if your
|
||||
/// project targets Python 3.10 and above, adding `from __future__ import annotations`
|
||||
/// does not impact your ability to leverage PEP 604-style unions (e.g., to
|
||||
/// convert `Optional[str]` to `str | None`). As such, this rule will only
|
||||
/// flag such usages if your project targets Python 3.9 or below.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def func(obj: dict[str, int | None]) -> None:
|
||||
@@ -34,6 +40,9 @@ use crate::checkers::ast::Checker;
|
||||
/// def func(obj: dict[str, int | None]) -> None:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `target-version`
|
||||
#[violation]
|
||||
pub struct FutureRequiredTypeAnnotation {
|
||||
reason: Reason,
|
||||
|
||||
@@ -13,14 +13,25 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// PEP 563 enabled the use of a number of convenient type annotations, such as
|
||||
/// `list[str]` instead of `List[str]`, or `str | None` instead of
|
||||
/// `Optional[str]`. However, these annotations are only available on Python
|
||||
/// 3.9 and higher, _unless_ the `from __future__ import annotations` import is present.
|
||||
/// `list[str]` instead of `List[str]`. However, these annotations are only
|
||||
/// available on Python 3.9 and higher, _unless_ the `from __future__ import annotations`
|
||||
/// import is present.
|
||||
///
|
||||
/// Similarly, PEP 604 enabled the use of the `|` operator for unions, such as
|
||||
/// `str | None` instead of `Optional[str]`. However, these annotations are only
|
||||
/// available on Python 3.10 and higher, _unless_ the `from __future__ import annotations`
|
||||
/// import is present.
|
||||
///
|
||||
/// By adding the `__future__` import, the pyupgrade rules can automatically
|
||||
/// migrate existing code to use the new syntax, even for older Python versions.
|
||||
/// This rule thus pairs well with pyupgrade and with Ruff's pyupgrade rules.
|
||||
///
|
||||
/// This rule respects the [`target-version`] setting. For example, if your
|
||||
/// project targets Python 3.10 and above, adding `from __future__ import annotations`
|
||||
/// does not impact your ability to leverage PEP 604-style unions (e.g., to
|
||||
/// convert `Optional[str]` to `str | None`). As such, this rule will only
|
||||
/// flag such usages if your project targets Python 3.9 or below.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from typing import List, Dict, Optional
|
||||
|
||||
@@ -43,6 +43,11 @@ impl Violation for UnnecessaryTypeUnion {
|
||||
|
||||
/// PYI055
|
||||
pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr) {
|
||||
// The `|` operator isn't always safe to allow to runtime-evaluated annotations.
|
||||
if checker.semantic().execution_context().is_runtime() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut type_exprs = Vec::new();
|
||||
|
||||
// Check if `union` is a PEP604 union (e.g. `float | int`) or a `typing.Union[float, int]`
|
||||
|
||||
@@ -123,7 +123,7 @@ pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) {
|
||||
}
|
||||
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
value: Constant::Str(ast::StringConstant { value, .. }),
|
||||
..
|
||||
}) = right
|
||||
{
|
||||
|
||||
@@ -1,56 +1,12 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI055.py:5:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
|
||||
|
|
||||
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
6 | x: type[int] | type[str] | type[float]
|
||||
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
|
|
||||
|
||||
PYI055.py:6:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | float]`.
|
||||
|
|
||||
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
6 | x: type[int] | type[str] | type[float]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
8 | z: Union[type[float], type[complex]]
|
||||
|
|
||||
|
||||
PYI055.py:7:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
|
||||
|
|
||||
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
6 | x: type[int] | type[str] | type[float]
|
||||
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
8 | z: Union[type[float], type[complex]]
|
||||
9 | z: Union[type[float, int], type[complex]]
|
||||
|
|
||||
|
||||
PYI055.py:8:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, complex]]`.
|
||||
|
|
||||
6 | x: type[int] | type[str] | type[float]
|
||||
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
8 | z: Union[type[float], type[complex]]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
9 | z: Union[type[float, int], type[complex]]
|
||||
|
|
||||
|
||||
PYI055.py:9:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, int, complex]]`.
|
||||
|
|
||||
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
8 | z: Union[type[float], type[complex]]
|
||||
9 | z: Union[type[float, int], type[complex]]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
|
|
||||
|
||||
PYI055.py:12:15: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | float]`.
|
||||
PYI055.py:31:11: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[requests_mock.Mocker | httpretty]`.
|
||||
|
|
||||
12 | def func(arg: type[int] | str | type[float]) -> None: ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
13 |
|
||||
14 | # OK
|
||||
29 | def func():
|
||||
30 | # PYI055
|
||||
31 | item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,56 +1,79 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI055.pyi:5:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
|
||||
PYI055.pyi:4:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
|
||||
|
|
||||
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
2 | from typing import Union
|
||||
3 |
|
||||
4 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
6 | x: type[int] | type[str] | type[float]
|
||||
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
5 | x: type[int] | type[str] | type[float]
|
||||
6 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
|
|
||||
|
||||
PYI055.pyi:6:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | float]`.
|
||||
PYI055.pyi:5:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | float]`.
|
||||
|
|
||||
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
6 | x: type[int] | type[str] | type[float]
|
||||
4 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
5 | x: type[int] | type[str] | type[float]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
8 | z: Union[type[float], type[complex]]
|
||||
6 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
7 | z: Union[type[float], type[complex]]
|
||||
|
|
||||
|
||||
PYI055.pyi:7:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
|
||||
PYI055.pyi:6:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
|
||||
|
|
||||
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
6 | x: type[int] | type[str] | type[float]
|
||||
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
4 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
5 | x: type[int] | type[str] | type[float]
|
||||
6 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
8 | z: Union[type[float], type[complex]]
|
||||
9 | z: Union[type[float, int], type[complex]]
|
||||
7 | z: Union[type[float], type[complex]]
|
||||
8 | z: Union[type[float, int], type[complex]]
|
||||
|
|
||||
|
||||
PYI055.pyi:8:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, complex]]`.
|
||||
PYI055.pyi:7:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, complex]]`.
|
||||
|
|
||||
6 | x: type[int] | type[str] | type[float]
|
||||
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
8 | z: Union[type[float], type[complex]]
|
||||
5 | x: type[int] | type[str] | type[float]
|
||||
6 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
7 | z: Union[type[float], type[complex]]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
9 | z: Union[type[float, int], type[complex]]
|
||||
8 | z: Union[type[float, int], type[complex]]
|
||||
|
|
||||
|
||||
PYI055.pyi:9:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, int, complex]]`.
|
||||
|
|
||||
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
8 | z: Union[type[float], type[complex]]
|
||||
9 | z: Union[type[float, int], type[complex]]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
|
|
||||
|
||||
PYI055.pyi:12:15: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | float]`.
|
||||
PYI055.pyi:8:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, int, complex]]`.
|
||||
|
|
||||
12 | def func(arg: type[int] | str | type[float]) -> None: ...
|
||||
6 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
7 | z: Union[type[float], type[complex]]
|
||||
8 | z: Union[type[float, int], type[complex]]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
9 |
|
||||
10 | def func(arg: type[int] | str | type[float]) -> None: ...
|
||||
|
|
||||
|
||||
PYI055.pyi:10:15: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | float]`.
|
||||
|
|
||||
8 | z: Union[type[float, int], type[complex]]
|
||||
9 |
|
||||
10 | def func(arg: type[int] | str | type[float]) -> None: ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
13 |
|
||||
14 | # OK
|
||||
11 |
|
||||
12 | # OK
|
||||
|
|
||||
|
||||
PYI055.pyi:20:7: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[requests_mock.Mocker | httpretty]`.
|
||||
|
|
||||
19 | # OK
|
||||
20 | item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
21 |
|
||||
22 | def func():
|
||||
|
|
||||
|
||||
PYI055.pyi:24:11: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[requests_mock.Mocker | httpretty]`.
|
||||
|
|
||||
22 | def func():
|
||||
23 | # PYI055
|
||||
24 | item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -169,6 +169,12 @@ mod tests {
|
||||
Settings::default(),
|
||||
"PT013"
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::PytestDuplicateParametrizeTestCases,
|
||||
Path::new("PT014.py"),
|
||||
Settings::default(),
|
||||
"PT014"
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::PytestAssertAlwaysFalse,
|
||||
Path::new("PT015.py"),
|
||||
@@ -250,6 +256,18 @@ mod tests {
|
||||
Settings::default(),
|
||||
"PT026"
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::PytestUnittestRaisesAssertion,
|
||||
Path::new("PT027_0.py"),
|
||||
Settings::default(),
|
||||
"PT027_0"
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::PytestUnittestRaisesAssertion,
|
||||
Path::new("PT027_1.py"),
|
||||
Settings::default(),
|
||||
"PT027_1"
|
||||
)]
|
||||
fn test_pytest_style(
|
||||
rule_code: Rule,
|
||||
path: &Path,
|
||||
|
||||
@@ -7,12 +7,14 @@ use libcst_native::{
|
||||
ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement,
|
||||
TrailingWhitespace, UnaryOperation,
|
||||
};
|
||||
use ruff_python_ast::{self as ast, BoolOp, ExceptHandler, Expr, Keyword, Ranged, Stmt, UnaryOp};
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::Truthiness;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Ranged, Stmt, UnaryOp,
|
||||
};
|
||||
use ruff_python_ast::{visitor, whitespace};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_source_file::Locator;
|
||||
@@ -21,6 +23,7 @@ use crate::autofix::codemods::CodegenStylist;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::cst::matchers::match_indented_block;
|
||||
use crate::cst::matchers::match_module;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
use super::unittest_assert::UnittestAssert;
|
||||
@@ -89,6 +92,9 @@ impl Violation for PytestCompositeAssertion {
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// def test_foo():
|
||||
/// with pytest.raises(ZeroDivisionError) as exc_info:
|
||||
/// 1 / 0
|
||||
@@ -127,6 +133,9 @@ impl Violation for PytestAssertInExcept {
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// def test_foo():
|
||||
/// if some_condition:
|
||||
/// pytest.fail("some_condition was True")
|
||||
@@ -145,6 +154,36 @@ impl Violation for PytestAssertAlwaysFalse {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of assertion methods from the `unittest` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// To make use of `pytest`'s assertion rewriting, a regular `assert` statement
|
||||
/// is preferred over `unittest`'s assertion methods.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import unittest
|
||||
///
|
||||
///
|
||||
/// class TestFoo(unittest.TestCase):
|
||||
/// def test_foo(self):
|
||||
/// self.assertEqual(a, b)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import unittest
|
||||
///
|
||||
///
|
||||
/// class TestFoo(unittest.TestCase):
|
||||
/// def test_foo(self):
|
||||
/// assert a == b
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: Assertion introspection details](https://docs.pytest.org/en/7.1.x/how-to/assert.html#assertion-introspection-details)
|
||||
|
||||
#[violation]
|
||||
pub struct PytestUnittestAssertion {
|
||||
assertion: String,
|
||||
@@ -267,6 +306,186 @@ pub(crate) fn unittest_assertion(
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of exception-related assertion methods from the `unittest`
|
||||
/// module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// To enforce the assertion style recommended by `pytest`, `pytest.raises` is
|
||||
/// preferred over the exception-related assertion methods in `unittest`, like
|
||||
/// `assertRaises`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import unittest
|
||||
///
|
||||
///
|
||||
/// class TestFoo(unittest.TestCase):
|
||||
/// def test_foo(self):
|
||||
/// with self.assertRaises(ValueError):
|
||||
/// raise ValueError("foo")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import unittest
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// class TestFoo(unittest.TestCase):
|
||||
/// def test_foo(self):
|
||||
/// with pytest.raises(ValueError):
|
||||
/// raise ValueError("foo")
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: Assertions about expected exceptions](https://docs.pytest.org/en/latest/how-to/assert.html#assertions-about-expected-exceptions)
|
||||
#[violation]
|
||||
pub struct PytestUnittestRaisesAssertion {
|
||||
assertion: String,
|
||||
}
|
||||
|
||||
impl Violation for PytestUnittestRaisesAssertion {
|
||||
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let PytestUnittestRaisesAssertion { assertion } = self;
|
||||
format!("Use `pytest.raises` instead of unittest-style `{assertion}`")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> Option<String> {
|
||||
let PytestUnittestRaisesAssertion { assertion } = self;
|
||||
Some(format!("Replace `{assertion}` with `pytest.raises`"))
|
||||
}
|
||||
}
|
||||
|
||||
/// PT027
|
||||
pub(crate) fn unittest_raises_assertion(
|
||||
checker: &Checker,
|
||||
call: &ast::ExprCall,
|
||||
) -> Option<Diagnostic> {
|
||||
let Expr::Attribute(ast::ExprAttribute { attr, .. }) = call.func.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if !matches!(
|
||||
attr.as_str(),
|
||||
"assertRaises" | "failUnlessRaises" | "assertRaisesRegex" | "assertRaisesRegexp"
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
PytestUnittestRaisesAssertion {
|
||||
assertion: attr.to_string(),
|
||||
},
|
||||
call.func.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule())
|
||||
&& !checker.indexer().has_comments(call, checker.locator())
|
||||
{
|
||||
if let Some(args) = to_pytest_raises_args(checker, attr.as_str(), &call.arguments) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import("pytest", "raises"),
|
||||
call.func.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let edit = Edit::range_replacement(format!("{binding}({args})"), call.range());
|
||||
Ok(Fix::suggested_edits(import_edit, [edit]))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some(diagnostic)
|
||||
}
|
||||
|
||||
fn to_pytest_raises_args<'a>(
|
||||
checker: &Checker<'a>,
|
||||
attr: &str,
|
||||
arguments: &Arguments,
|
||||
) -> Option<Cow<'a, str>> {
|
||||
let args = match attr {
|
||||
"assertRaises" | "failUnlessRaises" => {
|
||||
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
|
||||
// Ex) `assertRaises(Exception)`
|
||||
([arg], []) => Cow::Borrowed(checker.locator().slice(arg.range())),
|
||||
// Ex) `assertRaises(expected_exception=Exception)`
|
||||
([], [kwarg])
|
||||
if kwarg
|
||||
.arg
|
||||
.as_ref()
|
||||
.is_some_and(|id| id.as_str() == "expected_exception") =>
|
||||
{
|
||||
Cow::Borrowed(checker.locator().slice(kwarg.value.range()))
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
"assertRaisesRegex" | "assertRaisesRegexp" => {
|
||||
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
|
||||
// Ex) `assertRaisesRegex(Exception, regex)`
|
||||
([arg1, arg2], []) => Cow::Owned(format!(
|
||||
"{}, match={}",
|
||||
checker.locator().slice(arg1.range()),
|
||||
checker.locator().slice(arg2.range())
|
||||
)),
|
||||
// Ex) `assertRaisesRegex(Exception, expected_regex=regex)`
|
||||
([arg], [kwarg])
|
||||
if kwarg
|
||||
.arg
|
||||
.as_ref()
|
||||
.is_some_and(|arg| arg.as_str() == "expected_regex") =>
|
||||
{
|
||||
Cow::Owned(format!(
|
||||
"{}, match={}",
|
||||
checker.locator().slice(arg.range()),
|
||||
checker.locator().slice(kwarg.value.range())
|
||||
))
|
||||
}
|
||||
// Ex) `assertRaisesRegex(expected_exception=Exception, expected_regex=regex)`
|
||||
([], [kwarg1, kwarg2])
|
||||
if kwarg1
|
||||
.arg
|
||||
.as_ref()
|
||||
.is_some_and(|id| id.as_str() == "expected_exception")
|
||||
&& kwarg2
|
||||
.arg
|
||||
.as_ref()
|
||||
.is_some_and(|id| id.as_str() == "expected_regex") =>
|
||||
{
|
||||
Cow::Owned(format!(
|
||||
"{}, match={}",
|
||||
checker.locator().slice(kwarg1.value.range()),
|
||||
checker.locator().slice(kwarg2.value.range())
|
||||
))
|
||||
}
|
||||
// Ex) `assertRaisesRegex(expected_regex=regex, expected_exception=Exception)`
|
||||
([], [kwarg1, kwarg2])
|
||||
if kwarg1
|
||||
.arg
|
||||
.as_ref()
|
||||
.is_some_and(|id| id.as_str() == "expected_regex")
|
||||
&& kwarg2
|
||||
.arg
|
||||
.as_ref()
|
||||
.is_some_and(|id| id.as_str() == "expected_exception") =>
|
||||
{
|
||||
Cow::Owned(format!(
|
||||
"{}, match={}",
|
||||
checker.locator().slice(kwarg2.value.range()),
|
||||
checker.locator().slice(kwarg1.value.range())
|
||||
))
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(args)
|
||||
}
|
||||
|
||||
/// PT015
|
||||
pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
|
||||
if Truthiness::from_expr(test, |id| checker.semantic().is_builtin(id)).is_falsey() {
|
||||
|
||||
@@ -35,6 +35,9 @@ use super::helpers::{
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
@@ -42,6 +45,9 @@ use super::helpers::{
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
@@ -74,6 +80,35 @@ impl AlwaysAutofixableViolation for PytestFixtureIncorrectParenthesesStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `pytest.fixture` calls with positional arguments.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// For clarity and consistency, prefer using keyword arguments to specify
|
||||
/// fixture configuration.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture("module")
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture(scope="module")
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
|
||||
#[violation]
|
||||
pub struct PytestFixturePositionalArgs {
|
||||
function: String,
|
||||
@@ -87,6 +122,34 @@ impl Violation for PytestFixturePositionalArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `pytest.fixture` calls with `scope="function"`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `scope="function"` can be omitted, as it is the default.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture(scope="function")
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
|
||||
#[violation]
|
||||
pub struct PytestExtraneousScopeFunction;
|
||||
|
||||
@@ -101,6 +164,50 @@ impl AlwaysAutofixableViolation for PytestExtraneousScopeFunction {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `pytest` fixtures that do not return a value, but are not named
|
||||
/// with a leading underscore.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// By convention, fixtures that don't return a value should be named with a
|
||||
/// leading underscore, while fixtures that do return a value should not.
|
||||
///
|
||||
/// This rule ignores abstract fixtures and generators.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def patch_something(mocker):
|
||||
/// mocker.patch("module.object")
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def use_context():
|
||||
/// with create_context():
|
||||
/// yield
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def _patch_something(mocker):
|
||||
/// mocker.patch("module.object")
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def _use_context():
|
||||
/// with create_context():
|
||||
/// yield
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
|
||||
#[violation]
|
||||
pub struct PytestMissingFixtureNameUnderscore {
|
||||
function: String,
|
||||
@@ -114,6 +221,52 @@ impl Violation for PytestMissingFixtureNameUnderscore {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `pytest` fixtures that return a value, but are named with a
|
||||
/// leading underscore.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// By convention, fixtures that don't return a value should be named with a
|
||||
/// leading underscore, while fixtures that do return a value should not.
|
||||
///
|
||||
/// This rule ignores abstract fixtures.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def _some_object():
|
||||
/// return SomeClass()
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def _some_object_with_cleanup():
|
||||
/// obj = SomeClass()
|
||||
/// yield obj
|
||||
/// obj.cleanup()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def some_object():
|
||||
/// return SomeClass()
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def some_object_with_cleanup():
|
||||
/// obj = SomeClass()
|
||||
/// yield obj
|
||||
/// obj.cleanup()
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
|
||||
#[violation]
|
||||
pub struct PytestIncorrectFixtureNameUnderscore {
|
||||
function: String,
|
||||
@@ -145,6 +298,9 @@ impl Violation for PytestIncorrectFixtureNameUnderscore {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture
|
||||
/// def _patch_something():
|
||||
/// ...
|
||||
@@ -156,6 +312,9 @@ impl Violation for PytestIncorrectFixtureNameUnderscore {
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture
|
||||
/// def _patch_something():
|
||||
/// ...
|
||||
@@ -239,6 +398,9 @@ impl Violation for PytestDeprecatedYieldFixture {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def my_fixture(request):
|
||||
/// resource = acquire_resource()
|
||||
@@ -248,6 +410,9 @@ impl Violation for PytestDeprecatedYieldFixture {
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def my_fixture():
|
||||
/// resource = acquire_resource()
|
||||
@@ -288,6 +453,9 @@ impl Violation for PytestFixtureFinalizerCallback {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def my_fixture():
|
||||
/// resource = acquire_resource()
|
||||
@@ -296,6 +464,9 @@ impl Violation for PytestFixtureFinalizerCallback {
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def my_fixture_with_teardown():
|
||||
/// resource = acquire_resource()
|
||||
@@ -337,6 +508,9 @@ impl AlwaysAutofixableViolation for PytestUselessYieldFixture {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def a():
|
||||
/// pass
|
||||
@@ -350,6 +524,9 @@ impl AlwaysAutofixableViolation for PytestUselessYieldFixture {
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def a():
|
||||
/// pass
|
||||
@@ -384,6 +561,9 @@ impl AlwaysAutofixableViolation for PytestErroneousUseFixturesOnFixture {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.asyncio()
|
||||
/// @pytest.fixture()
|
||||
/// async def my_fixture():
|
||||
@@ -392,6 +572,9 @@ impl AlwaysAutofixableViolation for PytestErroneousUseFixturesOnFixture {
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// async def my_fixture():
|
||||
/// return 0
|
||||
|
||||
@@ -46,11 +46,11 @@ pub(super) fn is_pytest_parametrize(decorator: &Decorator, semantic: &SemanticMo
|
||||
|
||||
pub(super) fn keyword_is_literal(keyword: &Keyword, literal: &str) -> bool {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(string),
|
||||
value: Constant::Str(ast::StringConstant { value, .. }),
|
||||
..
|
||||
}) = &keyword.value
|
||||
{
|
||||
string == literal
|
||||
value == literal
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -63,7 +63,7 @@ pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool {
|
||||
..
|
||||
}) => string.is_empty(),
|
||||
Expr::Constant(constant) if constant.value.is_none() => true,
|
||||
Expr::FString(ast::ExprFString { values, range: _ }) => {
|
||||
Expr::FString(ast::ExprFString { values, .. }) => {
|
||||
values.iter().all(is_empty_or_null_string)
|
||||
}
|
||||
_ => false,
|
||||
|
||||
@@ -23,6 +23,9 @@ use super::helpers::get_mark_decorators;
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.foo
|
||||
/// def test_something():
|
||||
/// ...
|
||||
@@ -30,6 +33,9 @@ use super::helpers::get_mark_decorators;
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.foo()
|
||||
/// def test_something():
|
||||
/// ...
|
||||
@@ -76,6 +82,9 @@ impl AlwaysAutofixableViolation for PytestIncorrectMarkParenthesesStyle {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.usefixtures()
|
||||
/// def test_something():
|
||||
/// ...
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::hash::BuildHasherDefault;
|
||||
|
||||
use ruff_python_ast::{
|
||||
self as ast, Arguments, Constant, Decorator, Expr, ExprContext, PySourceType, Ranged,
|
||||
};
|
||||
@@ -6,6 +9,7 @@ use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
@@ -15,6 +19,58 @@ use crate::registry::{AsRule, Rule};
|
||||
use super::super::types;
|
||||
use super::helpers::{is_pytest_parametrize, split_names};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the type of parameter names passed to `pytest.mark.parametrize`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `argnames` argument of `pytest.mark.parametrize` takes a string or
|
||||
/// a sequence of strings. For a single parameter, it's preferable to use a
|
||||
/// string, and for multiple parameters, it's preferable to use the style
|
||||
/// configured via the `flake8-pytest-style.parametrize-names-type` setting.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// # single parameter, always expecting string
|
||||
/// @pytest.mark.parametrize(("param",), [1, 2, 3])
|
||||
/// def test_foo(param):
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// # multiple parameters, expecting tuple
|
||||
/// @pytest.mark.parametrize(["param1", "param2"], [(1, 2), (3, 4)])
|
||||
/// def test_bar(param1, param2):
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// # multiple parameters, expecting tuple
|
||||
/// @pytest.mark.parametrize("param1,param2", [(1, 2), (3, 4)])
|
||||
/// def test_baz(param1, param2):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize("param", [1, 2, 3])
|
||||
/// def test_foo(param):
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize(("param1", "param2"), [(1, 2), (3, 4)])
|
||||
/// def test_bar(param1, param2):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-pytest-style.parametrize-names-type`
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
|
||||
#[violation]
|
||||
pub struct PytestParametrizeNamesWrongType {
|
||||
pub expected: types::ParametrizeNameType,
|
||||
@@ -35,6 +91,71 @@ impl Violation for PytestParametrizeNamesWrongType {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the type of parameter values passed to `pytest.mark.parametrize`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `argvalues` argument of `pytest.mark.parametrize` takes an iterator of
|
||||
/// parameter values. For a single parameter, it's preferable to use a list,
|
||||
/// and for multiple parameters, it's preferable to use a list of rows with
|
||||
/// the type configured via the `flake8-pytest-style.parametrize-values-row-type`
|
||||
/// setting.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// # expected list, got tuple
|
||||
/// @pytest.mark.parametrize("param", (1, 2))
|
||||
/// def test_foo(param):
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// # expected top-level list, got tuple
|
||||
/// @pytest.mark.parametrize(
|
||||
/// ("param1", "param2"),
|
||||
/// (
|
||||
/// (1, 2),
|
||||
/// (3, 4),
|
||||
/// ),
|
||||
/// )
|
||||
/// def test_bar(param1, param2):
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// # expected individual rows to be tuples, got lists
|
||||
/// @pytest.mark.parametrize(
|
||||
/// ("param1", "param2"),
|
||||
/// [
|
||||
/// [1, 2],
|
||||
/// [3, 4],
|
||||
/// ],
|
||||
/// )
|
||||
/// def test_baz(param1, param2):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize("param", [1, 2, 3])
|
||||
/// def test_foo(param):
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize(("param1", "param2"), [(1, 2), (3, 4)])
|
||||
/// def test_bar(param1, param2):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-pytest-style.parametrize-values-row-type`
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
|
||||
#[violation]
|
||||
pub struct PytestParametrizeValuesWrongType {
|
||||
pub values: types::ParametrizeValuesType,
|
||||
@@ -49,10 +170,62 @@ impl Violation for PytestParametrizeValuesWrongType {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for duplicate test cases in `pytest.mark.parametrize`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Duplicate test cases are redundant and should be removed.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize(
|
||||
/// ("param1", "param2"),
|
||||
/// [
|
||||
/// (1, 2),
|
||||
/// (1, 2),
|
||||
/// ],
|
||||
/// )
|
||||
/// def test_foo(param1, param2):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize(
|
||||
/// ("param1", "param2"),
|
||||
/// [
|
||||
/// (1, 2),
|
||||
/// ],
|
||||
/// )
|
||||
/// def test_foo(param1, param2):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
|
||||
#[violation]
|
||||
pub struct PytestDuplicateParametrizeTestCases {
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl Violation for PytestDuplicateParametrizeTestCases {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let PytestDuplicateParametrizeTestCases { index } = self;
|
||||
format!("Duplicate of test case at index {index} in `@pytest_mark.parametrize`")
|
||||
}
|
||||
}
|
||||
|
||||
fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
|
||||
let all_literals = elts.iter().all(|e| {
|
||||
let all_literals = elts.iter().all(|expr| {
|
||||
matches!(
|
||||
e,
|
||||
expr,
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(_),
|
||||
..
|
||||
@@ -65,19 +238,23 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
|
||||
}
|
||||
|
||||
let node = Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(elts.iter().fold(String::new(), |mut acc, elt| {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(ref s),
|
||||
..
|
||||
}) = elt
|
||||
{
|
||||
if !acc.is_empty() {
|
||||
acc.push(',');
|
||||
value: elts
|
||||
.iter()
|
||||
.fold(String::new(), |mut acc, elt| {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(ast::StringConstant { value, .. }),
|
||||
..
|
||||
}) = elt
|
||||
{
|
||||
if !acc.is_empty() {
|
||||
acc.push(',');
|
||||
}
|
||||
acc.push_str(value.as_str());
|
||||
}
|
||||
acc.push_str(s);
|
||||
}
|
||||
acc
|
||||
})),
|
||||
acc
|
||||
})
|
||||
.into(),
|
||||
|
||||
kind: None,
|
||||
range: TextRange::default(),
|
||||
});
|
||||
@@ -166,7 +343,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
|
||||
.iter()
|
||||
.map(|name| {
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str((*name).to_string()),
|
||||
value: (*name).to_string().into(),
|
||||
kind: None,
|
||||
range: TextRange::default(),
|
||||
})
|
||||
@@ -201,7 +378,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
|
||||
.iter()
|
||||
.map(|name| {
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str((*name).to_string()),
|
||||
value: (*name).to_string().into(),
|
||||
kind: None,
|
||||
range: TextRange::default(),
|
||||
})
|
||||
@@ -351,6 +528,7 @@ fn check_values(checker: &mut Checker, names: &Expr, values: &Expr) {
|
||||
values.range(),
|
||||
));
|
||||
}
|
||||
|
||||
if is_multi_named {
|
||||
handle_value_rows(checker, elts, values_type, values_row_type);
|
||||
}
|
||||
@@ -373,6 +551,29 @@ fn check_values(checker: &mut Checker, names: &Expr, values: &Expr) {
|
||||
}
|
||||
}
|
||||
|
||||
/// PT014
|
||||
fn check_duplicates(checker: &mut Checker, values: &Expr) {
|
||||
let (Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. })) =
|
||||
values
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut seen: FxHashMap<ComparableExpr, usize> =
|
||||
FxHashMap::with_capacity_and_hasher(elts.len(), BuildHasherDefault::default());
|
||||
for (index, elt) in elts.iter().enumerate() {
|
||||
let expr = ComparableExpr::from(elt);
|
||||
seen.entry(expr)
|
||||
.and_modify(|index| {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
PytestDuplicateParametrizeTestCases { index: *index },
|
||||
elt.range(),
|
||||
));
|
||||
})
|
||||
.or_insert(index);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_single_name(checker: &mut Checker, expr: &Expr, value: &Expr) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
PytestParametrizeNamesWrongType {
|
||||
@@ -446,6 +647,11 @@ pub(crate) fn parametrize(checker: &mut Checker, decorators: &[Decorator]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::PytestDuplicateParametrizeTestCases) {
|
||||
if let [_, values, ..] = &args[..] {
|
||||
check_duplicates(checker, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,39 @@ use ruff_python_ast::visitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr, Parameters, Ranged};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for mocked calls that use a dummy `lambda` function instead of
|
||||
/// `return_value`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// When patching calls, an explicit `return_value` better conveys the intent
|
||||
/// than a `lambda` function, assuming the `lambda` does not use the arguments
|
||||
/// passed to it.
|
||||
///
|
||||
/// `return_value` is also robust to changes in the patched function's
|
||||
/// signature, and enables additional assertions to verify behavior. For
|
||||
/// example, `return_value` allows for verification of the number of calls or
|
||||
/// the arguments passed to the patched function via `assert_called_once_with`
|
||||
/// and related methods.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def test_foo(mocker):
|
||||
/// mocker.patch("module.target", lambda x, y: 7)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def test_foo(mocker):
|
||||
/// mocker.patch("module.target", return_value=7)
|
||||
///
|
||||
/// # If the lambda makes use of the arguments, no diagnostic is emitted.
|
||||
/// mocker.patch("module.other_target", lambda x, y: x)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `unittest.mock.patch`](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch)
|
||||
/// - [`pytest-mock`](https://pypi.org/project/pytest-mock/)
|
||||
#[violation]
|
||||
pub struct PytestPatchWithLambda;
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ use super::helpers::is_empty_or_null_string;
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// def test_foo():
|
||||
/// with pytest.raises(MyError):
|
||||
/// setup() # may raise `MyError`
|
||||
@@ -31,6 +34,9 @@ use super::helpers::is_empty_or_null_string;
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// def test_foo():
|
||||
/// setup()
|
||||
/// with pytest.raises(MyException):
|
||||
@@ -63,6 +69,9 @@ impl Violation for PytestRaisesWithMultipleStatements {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// def test_foo():
|
||||
/// with pytest.raises(ValueError):
|
||||
/// ...
|
||||
@@ -74,6 +83,9 @@ impl Violation for PytestRaisesWithMultipleStatements {
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// def test_foo():
|
||||
/// with pytest.raises(ValueError, match="expected message"):
|
||||
/// ...
|
||||
|
||||
@@ -19,6 +19,13 @@ pub(crate) enum UnittestAssert {
|
||||
DictEqual,
|
||||
Equal,
|
||||
Equals,
|
||||
FailIf,
|
||||
FailIfAlmostEqual,
|
||||
FailIfEqual,
|
||||
FailUnless,
|
||||
FailUnlessAlmostEqual,
|
||||
FailUnlessEqual,
|
||||
// FailUnlessRaises,
|
||||
False,
|
||||
Greater,
|
||||
GreaterEqual,
|
||||
@@ -62,10 +69,16 @@ impl std::fmt::Display for UnittestAssert {
|
||||
UnittestAssert::AlmostEqual => write!(f, "assertAlmostEqual"),
|
||||
UnittestAssert::AlmostEquals => write!(f, "assertAlmostEquals"),
|
||||
UnittestAssert::CountEqual => write!(f, "assertCountEqual"),
|
||||
UnittestAssert::DictEqual => write!(f, "assertDictEqual"),
|
||||
UnittestAssert::DictContainsSubset => write!(f, "assertDictContainsSubset"),
|
||||
UnittestAssert::DictEqual => write!(f, "assertDictEqual"),
|
||||
UnittestAssert::Equal => write!(f, "assertEqual"),
|
||||
UnittestAssert::Equals => write!(f, "assertEquals"),
|
||||
UnittestAssert::FailIf => write!(f, "failIf"),
|
||||
UnittestAssert::FailIfAlmostEqual => write!(f, "failIfAlmostEqual"),
|
||||
UnittestAssert::FailIfEqual => write!(f, "failIfEqual"),
|
||||
UnittestAssert::FailUnless => write!(f, "failUnless"),
|
||||
UnittestAssert::FailUnlessAlmostEqual => write!(f, "failUnlessAlmostEqual"),
|
||||
UnittestAssert::FailUnlessEqual => write!(f, "failUnlessEqual"),
|
||||
UnittestAssert::False => write!(f, "assertFalse"),
|
||||
UnittestAssert::Greater => write!(f, "assertGreater"),
|
||||
UnittestAssert::GreaterEqual => write!(f, "assertGreaterEqual"),
|
||||
@@ -110,6 +123,12 @@ impl TryFrom<&str> for UnittestAssert {
|
||||
"assertDictEqual" => Ok(UnittestAssert::DictEqual),
|
||||
"assertEqual" => Ok(UnittestAssert::Equal),
|
||||
"assertEquals" => Ok(UnittestAssert::Equals),
|
||||
"failIf" => Ok(UnittestAssert::FailIf),
|
||||
"failIfAlmostEqual" => Ok(UnittestAssert::FailIfAlmostEqual),
|
||||
"failIfEqual" => Ok(UnittestAssert::FailIfEqual),
|
||||
"failUnless" => Ok(UnittestAssert::FailUnless),
|
||||
"failUnlessAlmostEqual" => Ok(UnittestAssert::FailUnlessAlmostEqual),
|
||||
"failUnlessEqual" => Ok(UnittestAssert::FailUnlessEqual),
|
||||
"assertFalse" => Ok(UnittestAssert::False),
|
||||
"assertGreater" => Ok(UnittestAssert::Greater),
|
||||
"assertGreaterEqual" => Ok(UnittestAssert::GreaterEqual),
|
||||
@@ -198,6 +217,12 @@ impl UnittestAssert {
|
||||
UnittestAssert::True => &["expr", "msg"],
|
||||
UnittestAssert::TupleEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::Underscore => &["expr", "msg"],
|
||||
UnittestAssert::FailIf => &["expr", "msg"],
|
||||
UnittestAssert::FailIfAlmostEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::FailIfEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::FailUnless => &["expr", "msg"],
|
||||
UnittestAssert::FailUnlessAlmostEqual => &["first", "second", "places", "msg", "delta"],
|
||||
UnittestAssert::FailUnlessEqual => &["first", "second", "places", "msg", "delta"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,28 +282,35 @@ impl UnittestAssert {
|
||||
pub(crate) fn generate_assert(self, args: &[Expr], keywords: &[Keyword]) -> Result<Stmt> {
|
||||
let args = self.args_map(args, keywords)?;
|
||||
match self {
|
||||
UnittestAssert::True | UnittestAssert::False => {
|
||||
UnittestAssert::True
|
||||
| UnittestAssert::False
|
||||
| UnittestAssert::FailUnless
|
||||
| UnittestAssert::FailIf => {
|
||||
let expr = *args
|
||||
.get("expr")
|
||||
.ok_or_else(|| anyhow!("Missing argument `expr`"))?;
|
||||
let msg = args.get("msg").copied();
|
||||
Ok(if matches!(self, UnittestAssert::False) {
|
||||
assert(
|
||||
&Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: UnaryOp::Not,
|
||||
operand: Box::new(expr.clone()),
|
||||
range: TextRange::default(),
|
||||
}),
|
||||
msg,
|
||||
)
|
||||
} else {
|
||||
assert(expr, msg)
|
||||
})
|
||||
Ok(
|
||||
if matches!(self, UnittestAssert::False | UnittestAssert::FailIf) {
|
||||
assert(
|
||||
&Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: UnaryOp::Not,
|
||||
operand: Box::new(expr.clone()),
|
||||
range: TextRange::default(),
|
||||
}),
|
||||
msg,
|
||||
)
|
||||
} else {
|
||||
assert(expr, msg)
|
||||
},
|
||||
)
|
||||
}
|
||||
UnittestAssert::Equal
|
||||
| UnittestAssert::Equals
|
||||
| UnittestAssert::FailUnlessEqual
|
||||
| UnittestAssert::NotEqual
|
||||
| UnittestAssert::NotEquals
|
||||
| UnittestAssert::FailIfEqual
|
||||
| UnittestAssert::Greater
|
||||
| UnittestAssert::GreaterEqual
|
||||
| UnittestAssert::Less
|
||||
@@ -293,8 +325,12 @@ impl UnittestAssert {
|
||||
.ok_or_else(|| anyhow!("Missing argument `second`"))?;
|
||||
let msg = args.get("msg").copied();
|
||||
let cmp_op = match self {
|
||||
UnittestAssert::Equal | UnittestAssert::Equals => CmpOp::Eq,
|
||||
UnittestAssert::NotEqual | UnittestAssert::NotEquals => CmpOp::NotEq,
|
||||
UnittestAssert::Equal
|
||||
| UnittestAssert::Equals
|
||||
| UnittestAssert::FailUnlessEqual => CmpOp::Eq,
|
||||
UnittestAssert::NotEqual
|
||||
| UnittestAssert::NotEquals
|
||||
| UnittestAssert::FailIfEqual => CmpOp::NotEq,
|
||||
UnittestAssert::Greater => CmpOp::Gt,
|
||||
UnittestAssert::GreaterEqual => CmpOp::GtE,
|
||||
UnittestAssert::Less => CmpOp::Lt,
|
||||
|
||||
@@ -548,6 +548,8 @@ PT009.py:82:9: PT009 [*] Use a regular `assert` instead of unittest-style `asser
|
||||
81 | def test_assert_not_regexp_matches(self):
|
||||
82 | self.assertNotRegex("abc", r"abc") # Error
|
||||
| ^^^^^^^^^^^^^^^^^^^ PT009
|
||||
83 |
|
||||
84 | def test_fail_if(self):
|
||||
|
|
||||
= help: Replace `assertNotRegex(...)` with `assert ...`
|
||||
|
||||
@@ -557,5 +559,83 @@ PT009.py:82:9: PT009 [*] Use a regular `assert` instead of unittest-style `asser
|
||||
81 81 | def test_assert_not_regexp_matches(self):
|
||||
82 |- self.assertNotRegex("abc", r"abc") # Error
|
||||
82 |+ assert not re.search("abc", "abc") # Error
|
||||
83 83 |
|
||||
84 84 | def test_fail_if(self):
|
||||
85 85 | self.failIf("abc") # Error
|
||||
|
||||
PT009.py:85:9: PT009 [*] Use a regular `assert` instead of unittest-style `failIf`
|
||||
|
|
||||
84 | def test_fail_if(self):
|
||||
85 | self.failIf("abc") # Error
|
||||
| ^^^^^^^^^^^ PT009
|
||||
86 |
|
||||
87 | def test_fail_unless(self):
|
||||
|
|
||||
= help: Replace `failIf(...)` with `assert ...`
|
||||
|
||||
ℹ Suggested fix
|
||||
82 82 | self.assertNotRegex("abc", r"abc") # Error
|
||||
83 83 |
|
||||
84 84 | def test_fail_if(self):
|
||||
85 |- self.failIf("abc") # Error
|
||||
85 |+ assert not "abc" # Error
|
||||
86 86 |
|
||||
87 87 | def test_fail_unless(self):
|
||||
88 88 | self.failUnless("abc") # Error
|
||||
|
||||
PT009.py:88:9: PT009 [*] Use a regular `assert` instead of unittest-style `failUnless`
|
||||
|
|
||||
87 | def test_fail_unless(self):
|
||||
88 | self.failUnless("abc") # Error
|
||||
| ^^^^^^^^^^^^^^^ PT009
|
||||
89 |
|
||||
90 | def test_fail_unless_equal(self):
|
||||
|
|
||||
= help: Replace `failUnless(...)` with `assert ...`
|
||||
|
||||
ℹ Suggested fix
|
||||
85 85 | self.failIf("abc") # Error
|
||||
86 86 |
|
||||
87 87 | def test_fail_unless(self):
|
||||
88 |- self.failUnless("abc") # Error
|
||||
88 |+ assert "abc" # Error
|
||||
89 89 |
|
||||
90 90 | def test_fail_unless_equal(self):
|
||||
91 91 | self.failUnlessEqual(1, 2) # Error
|
||||
|
||||
PT009.py:91:9: PT009 [*] Use a regular `assert` instead of unittest-style `failUnlessEqual`
|
||||
|
|
||||
90 | def test_fail_unless_equal(self):
|
||||
91 | self.failUnlessEqual(1, 2) # Error
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PT009
|
||||
92 |
|
||||
93 | def test_fail_if_equal(self):
|
||||
|
|
||||
= help: Replace `failUnlessEqual(...)` with `assert ...`
|
||||
|
||||
ℹ Suggested fix
|
||||
88 88 | self.failUnless("abc") # Error
|
||||
89 89 |
|
||||
90 90 | def test_fail_unless_equal(self):
|
||||
91 |- self.failUnlessEqual(1, 2) # Error
|
||||
91 |+ assert 1 == 2 # Error
|
||||
92 92 |
|
||||
93 93 | def test_fail_if_equal(self):
|
||||
94 94 | self.failIfEqual(1, 2) # Error
|
||||
|
||||
PT009.py:94:9: PT009 [*] Use a regular `assert` instead of unittest-style `failIfEqual`
|
||||
|
|
||||
93 | def test_fail_if_equal(self):
|
||||
94 | self.failIfEqual(1, 2) # Error
|
||||
| ^^^^^^^^^^^^^^^^ PT009
|
||||
|
|
||||
= help: Replace `failIfEqual(...)` with `assert ...`
|
||||
|
||||
ℹ Suggested fix
|
||||
91 91 | self.failUnlessEqual(1, 2) # Error
|
||||
92 92 |
|
||||
93 93 | def test_fail_if_equal(self):
|
||||
94 |- self.failIfEqual(1, 2) # Error
|
||||
94 |+ assert 1 != 2 # Error
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
|
||||
---
|
||||
PT014.py:4:35: PT014 Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
|
|
||||
4 | @pytest.mark.parametrize("x", [1, 1, 2])
|
||||
| ^ PT014
|
||||
5 | def test_error_literal(x):
|
||||
6 | ...
|
||||
|
|
||||
|
||||
PT014.py:14:35: PT014 Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
|
|
||||
14 | @pytest.mark.parametrize("x", [a, a, b, b, b, c])
|
||||
| ^ PT014
|
||||
15 | def test_error_expr_simple(x):
|
||||
16 | ...
|
||||
|
|
||||
|
||||
PT014.py:14:41: PT014 Duplicate of test case at index 2 in `@pytest_mark.parametrize`
|
||||
|
|
||||
14 | @pytest.mark.parametrize("x", [a, a, b, b, b, c])
|
||||
| ^ PT014
|
||||
15 | def test_error_expr_simple(x):
|
||||
16 | ...
|
||||
|
|
||||
|
||||
PT014.py:14:44: PT014 Duplicate of test case at index 2 in `@pytest_mark.parametrize`
|
||||
|
|
||||
14 | @pytest.mark.parametrize("x", [a, a, b, b, b, c])
|
||||
| ^ PT014
|
||||
15 | def test_error_expr_simple(x):
|
||||
16 | ...
|
||||
|
|
||||
|
||||
PT014.py:19:40: PT014 Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
||||
|
|
||||
19 | @pytest.mark.parametrize("x", [(a, b), (a, b), (b, c)])
|
||||
| ^^^^^^ PT014
|
||||
20 | def test_error_expr_complex(x):
|
||||
21 | ...
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
|
||||
---
|
||||
PT027_0.py:6:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaises`
|
||||
|
|
||||
4 | class Test(unittest.TestCase):
|
||||
5 | def test_errors(self):
|
||||
6 | with self.assertRaises(ValueError):
|
||||
| ^^^^^^^^^^^^^^^^^ PT027
|
||||
7 | raise ValueError
|
||||
8 | with self.assertRaises(expected_exception=ValueError):
|
||||
|
|
||||
= help: Replace `assertRaises` with `pytest.raises`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | import unittest
|
||||
2 |+import pytest
|
||||
2 3 |
|
||||
3 4 |
|
||||
4 5 | class Test(unittest.TestCase):
|
||||
5 6 | def test_errors(self):
|
||||
6 |- with self.assertRaises(ValueError):
|
||||
7 |+ with pytest.raises(ValueError):
|
||||
7 8 | raise ValueError
|
||||
8 9 | with self.assertRaises(expected_exception=ValueError):
|
||||
9 10 | raise ValueError
|
||||
|
||||
PT027_0.py:8:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaises`
|
||||
|
|
||||
6 | with self.assertRaises(ValueError):
|
||||
7 | raise ValueError
|
||||
8 | with self.assertRaises(expected_exception=ValueError):
|
||||
| ^^^^^^^^^^^^^^^^^ PT027
|
||||
9 | raise ValueError
|
||||
|
|
||||
= help: Replace `assertRaises` with `pytest.raises`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | import unittest
|
||||
2 |+import pytest
|
||||
2 3 |
|
||||
3 4 |
|
||||
4 5 | class Test(unittest.TestCase):
|
||||
5 6 | def test_errors(self):
|
||||
6 7 | with self.assertRaises(ValueError):
|
||||
7 8 | raise ValueError
|
||||
8 |- with self.assertRaises(expected_exception=ValueError):
|
||||
9 |+ with pytest.raises(ValueError):
|
||||
9 10 | raise ValueError
|
||||
10 11 |
|
||||
11 12 | with self.failUnlessRaises(ValueError):
|
||||
|
||||
PT027_0.py:11:14: PT027 [*] Use `pytest.raises` instead of unittest-style `failUnlessRaises`
|
||||
|
|
||||
9 | raise ValueError
|
||||
10 |
|
||||
11 | with self.failUnlessRaises(ValueError):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ PT027
|
||||
12 | raise ValueError
|
||||
|
|
||||
= help: Replace `failUnlessRaises` with `pytest.raises`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | import unittest
|
||||
2 |+import pytest
|
||||
2 3 |
|
||||
3 4 |
|
||||
4 5 | class Test(unittest.TestCase):
|
||||
--------------------------------------------------------------------------------
|
||||
8 9 | with self.assertRaises(expected_exception=ValueError):
|
||||
9 10 | raise ValueError
|
||||
10 11 |
|
||||
11 |- with self.failUnlessRaises(ValueError):
|
||||
12 |+ with pytest.raises(ValueError):
|
||||
12 13 | raise ValueError
|
||||
13 14 |
|
||||
14 15 | with self.assertRaisesRegex(ValueError, "test"):
|
||||
|
||||
PT027_0.py:14:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex`
|
||||
|
|
||||
12 | raise ValueError
|
||||
13 |
|
||||
14 | with self.assertRaisesRegex(ValueError, "test"):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ PT027
|
||||
15 | raise ValueError("test")
|
||||
|
|
||||
= help: Replace `assertRaisesRegex` with `pytest.raises`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | import unittest
|
||||
2 |+import pytest
|
||||
2 3 |
|
||||
3 4 |
|
||||
4 5 | class Test(unittest.TestCase):
|
||||
--------------------------------------------------------------------------------
|
||||
11 12 | with self.failUnlessRaises(ValueError):
|
||||
12 13 | raise ValueError
|
||||
13 14 |
|
||||
14 |- with self.assertRaisesRegex(ValueError, "test"):
|
||||
15 |+ with pytest.raises(ValueError, match="test"):
|
||||
15 16 | raise ValueError("test")
|
||||
16 17 |
|
||||
17 18 | with self.assertRaisesRegex(ValueError, expected_regex="test"):
|
||||
|
||||
PT027_0.py:17:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex`
|
||||
|
|
||||
15 | raise ValueError("test")
|
||||
16 |
|
||||
17 | with self.assertRaisesRegex(ValueError, expected_regex="test"):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ PT027
|
||||
18 | raise ValueError("test")
|
||||
|
|
||||
= help: Replace `assertRaisesRegex` with `pytest.raises`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | import unittest
|
||||
2 |+import pytest
|
||||
2 3 |
|
||||
3 4 |
|
||||
4 5 | class Test(unittest.TestCase):
|
||||
--------------------------------------------------------------------------------
|
||||
14 15 | with self.assertRaisesRegex(ValueError, "test"):
|
||||
15 16 | raise ValueError("test")
|
||||
16 17 |
|
||||
17 |- with self.assertRaisesRegex(ValueError, expected_regex="test"):
|
||||
18 |+ with pytest.raises(ValueError, match="test"):
|
||||
18 19 | raise ValueError("test")
|
||||
19 20 |
|
||||
20 21 | with self.assertRaisesRegex(
|
||||
|
||||
PT027_0.py:20:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex`
|
||||
|
|
||||
18 | raise ValueError("test")
|
||||
19 |
|
||||
20 | with self.assertRaisesRegex(
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ PT027
|
||||
21 | expected_exception=ValueError, expected_regex="test"
|
||||
22 | ):
|
||||
|
|
||||
= help: Replace `assertRaisesRegex` with `pytest.raises`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | import unittest
|
||||
2 |+import pytest
|
||||
2 3 |
|
||||
3 4 |
|
||||
4 5 | class Test(unittest.TestCase):
|
||||
--------------------------------------------------------------------------------
|
||||
17 18 | with self.assertRaisesRegex(ValueError, expected_regex="test"):
|
||||
18 19 | raise ValueError("test")
|
||||
19 20 |
|
||||
20 |- with self.assertRaisesRegex(
|
||||
21 |- expected_exception=ValueError, expected_regex="test"
|
||||
22 |- ):
|
||||
21 |+ with pytest.raises(ValueError, match="test"):
|
||||
23 22 | raise ValueError("test")
|
||||
24 23 |
|
||||
25 24 | with self.assertRaisesRegex(
|
||||
|
||||
PT027_0.py:25:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex`
|
||||
|
|
||||
23 | raise ValueError("test")
|
||||
24 |
|
||||
25 | with self.assertRaisesRegex(
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ PT027
|
||||
26 | expected_regex="test", expected_exception=ValueError
|
||||
27 | ):
|
||||
|
|
||||
= help: Replace `assertRaisesRegex` with `pytest.raises`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | import unittest
|
||||
2 |+import pytest
|
||||
2 3 |
|
||||
3 4 |
|
||||
4 5 | class Test(unittest.TestCase):
|
||||
--------------------------------------------------------------------------------
|
||||
22 23 | ):
|
||||
23 24 | raise ValueError("test")
|
||||
24 25 |
|
||||
25 |- with self.assertRaisesRegex(
|
||||
26 |- expected_regex="test", expected_exception=ValueError
|
||||
27 |- ):
|
||||
26 |+ with pytest.raises(ValueError, match="test"):
|
||||
28 27 | raise ValueError("test")
|
||||
29 28 |
|
||||
30 29 | with self.assertRaisesRegexp(ValueError, "test"):
|
||||
|
||||
PT027_0.py:30:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegexp`
|
||||
|
|
||||
28 | raise ValueError("test")
|
||||
29 |
|
||||
30 | with self.assertRaisesRegexp(ValueError, "test"):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ PT027
|
||||
31 | raise ValueError("test")
|
||||
|
|
||||
= help: Replace `assertRaisesRegexp` with `pytest.raises`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | import unittest
|
||||
2 |+import pytest
|
||||
2 3 |
|
||||
3 4 |
|
||||
4 5 | class Test(unittest.TestCase):
|
||||
--------------------------------------------------------------------------------
|
||||
27 28 | ):
|
||||
28 29 | raise ValueError("test")
|
||||
29 30 |
|
||||
30 |- with self.assertRaisesRegexp(ValueError, "test"):
|
||||
31 |+ with pytest.raises(ValueError, match="test"):
|
||||
31 32 | raise ValueError("test")
|
||||
32 33 |
|
||||
33 34 | def test_unfixable_errors(self):
|
||||
|
||||
PT027_0.py:34:14: PT027 Use `pytest.raises` instead of unittest-style `assertRaises`
|
||||
|
|
||||
33 | def test_unfixable_errors(self):
|
||||
34 | with self.assertRaises(ValueError, msg="msg"):
|
||||
| ^^^^^^^^^^^^^^^^^ PT027
|
||||
35 | raise ValueError
|
||||
|
|
||||
= help: Replace `assertRaises` with `pytest.raises`
|
||||
|
||||
PT027_0.py:37:14: PT027 Use `pytest.raises` instead of unittest-style `assertRaises`
|
||||
|
|
||||
35 | raise ValueError
|
||||
36 |
|
||||
37 | with self.assertRaises(
|
||||
| ^^^^^^^^^^^^^^^^^ PT027
|
||||
38 | # comment
|
||||
39 | ValueError
|
||||
|
|
||||
= help: Replace `assertRaises` with `pytest.raises`
|
||||
|
||||
PT027_0.py:44:13: PT027 Use `pytest.raises` instead of unittest-style `assertRaises`
|
||||
|
|
||||
43 | with (
|
||||
44 | self
|
||||
| _____________^
|
||||
45 | | # comment
|
||||
46 | | .assertRaises(ValueError)
|
||||
| |_________________________^ PT027
|
||||
47 | ):
|
||||
48 | raise ValueError
|
||||
|
|
||||
= help: Replace `assertRaises` with `pytest.raises`
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
|
||||
---
|
||||
PT027_1.py:11:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaises`
|
||||
|
|
||||
10 | def test_errors(self):
|
||||
11 | with self.assertRaises(ValueError):
|
||||
| ^^^^^^^^^^^^^^^^^ PT027
|
||||
12 | raise ValueError
|
||||
|
|
||||
= help: Replace `assertRaises` with `pytest.raises`
|
||||
|
||||
ℹ Suggested fix
|
||||
8 8 | raise ValueError
|
||||
9 9 |
|
||||
10 10 | def test_errors(self):
|
||||
11 |- with self.assertRaises(ValueError):
|
||||
11 |+ with pytest.raises(ValueError):
|
||||
12 12 | raise ValueError
|
||||
|
||||
|
||||
@@ -457,7 +457,7 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) {
|
||||
implicit_return(checker, last_stmt);
|
||||
}
|
||||
}
|
||||
Stmt::Return(_) | Stmt::Raise(_) | Stmt::Try(_) | Stmt::TryStar(_) => {}
|
||||
Stmt::Return(_) | Stmt::Raise(_) | Stmt::Try(_) => {}
|
||||
Stmt::Expr(ast::StmtExpr { value, .. })
|
||||
if matches!(
|
||||
value.as_ref(),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use ruff_python_ast::{self as ast, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::collect_call_path;
|
||||
use ruff_python_semantic::ScopeKind;
|
||||
use ruff_python_ast::{self as ast, Expr, Ranged};
|
||||
use ruff_python_semantic::{BindingKind, ScopeKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -62,125 +61,132 @@ impl Violation for PrivateMemberAccess {
|
||||
|
||||
/// SLF001
|
||||
pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) {
|
||||
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = expr {
|
||||
if (attr.starts_with("__") && !attr.ends_with("__"))
|
||||
|| (attr.starts_with('_') && !attr.starts_with("__"))
|
||||
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = expr else {
|
||||
return;
|
||||
};
|
||||
|
||||
if (attr.starts_with("__") && !attr.ends_with("__"))
|
||||
|| (attr.starts_with('_') && !attr.starts_with("__"))
|
||||
{
|
||||
if checker
|
||||
.settings
|
||||
.flake8_self
|
||||
.ignore_names
|
||||
.contains(attr.as_ref())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore accesses on instances within special methods (e.g., `__eq__`).
|
||||
if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) =
|
||||
checker.semantic().current_scope().kind
|
||||
{
|
||||
if matches!(
|
||||
name.as_str(),
|
||||
"__lt__"
|
||||
| "__le__"
|
||||
| "__eq__"
|
||||
| "__ne__"
|
||||
| "__gt__"
|
||||
| "__ge__"
|
||||
| "__add__"
|
||||
| "__sub__"
|
||||
| "__mul__"
|
||||
| "__matmul__"
|
||||
| "__truediv__"
|
||||
| "__floordiv__"
|
||||
| "__mod__"
|
||||
| "__divmod__"
|
||||
| "__pow__"
|
||||
| "__lshift__"
|
||||
| "__rshift__"
|
||||
| "__and__"
|
||||
| "__xor__"
|
||||
| "__or__"
|
||||
| "__radd__"
|
||||
| "__rsub__"
|
||||
| "__rmul__"
|
||||
| "__rmatmul__"
|
||||
| "__rtruediv__"
|
||||
| "__rfloordiv__"
|
||||
| "__rmod__"
|
||||
| "__rdivmod__"
|
||||
| "__rpow__"
|
||||
| "__rlshift__"
|
||||
| "__rrshift__"
|
||||
| "__rand__"
|
||||
| "__rxor__"
|
||||
| "__ror__"
|
||||
| "__iadd__"
|
||||
| "__isub__"
|
||||
| "__imul__"
|
||||
| "__imatmul__"
|
||||
| "__itruediv__"
|
||||
| "__ifloordiv__"
|
||||
| "__imod__"
|
||||
| "__ipow__"
|
||||
| "__ilshift__"
|
||||
| "__irshift__"
|
||||
| "__iand__"
|
||||
| "__ixor__"
|
||||
| "__ior__"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow some documented private methods, like `os._exit()`.
|
||||
if let Some(call_path) = checker.semantic().resolve_call_path(expr) {
|
||||
if matches!(call_path.as_slice(), ["os", "_exit"]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
|
||||
// Ignore `super()` calls.
|
||||
if let Some(call_path) = collect_call_path(func) {
|
||||
if matches!(call_path.as_slice(), ["super"]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(call_path) = collect_call_path(value) {
|
||||
// Ignore `self` and `cls` accesses.
|
||||
if matches!(call_path.as_slice(), ["self" | "cls" | "mcs"]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Expr::Name(name) = value.as_ref() {
|
||||
// Ignore accesses on class members from _within_ the class.
|
||||
if checker
|
||||
.settings
|
||||
.flake8_self
|
||||
.ignore_names
|
||||
.contains(attr.as_ref())
|
||||
.semantic()
|
||||
.resolve_name(name)
|
||||
.and_then(|id| {
|
||||
if let BindingKind::ClassDefinition(scope) = checker.semantic().binding(id).kind
|
||||
{
|
||||
Some(scope)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.is_some_and(|scope| {
|
||||
checker
|
||||
.semantic()
|
||||
.current_scope_ids()
|
||||
.any(|parent| scope == parent)
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore accesses on instances within special methods (e.g., `__eq__`).
|
||||
if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) =
|
||||
checker.semantic().current_scope().kind
|
||||
{
|
||||
if matches!(
|
||||
name.as_str(),
|
||||
"__lt__"
|
||||
| "__le__"
|
||||
| "__eq__"
|
||||
| "__ne__"
|
||||
| "__gt__"
|
||||
| "__ge__"
|
||||
| "__add__"
|
||||
| "__sub__"
|
||||
| "__mul__"
|
||||
| "__matmul__"
|
||||
| "__truediv__"
|
||||
| "__floordiv__"
|
||||
| "__mod__"
|
||||
| "__divmod__"
|
||||
| "__pow__"
|
||||
| "__lshift__"
|
||||
| "__rshift__"
|
||||
| "__and__"
|
||||
| "__xor__"
|
||||
| "__or__"
|
||||
| "__radd__"
|
||||
| "__rsub__"
|
||||
| "__rmul__"
|
||||
| "__rmatmul__"
|
||||
| "__rtruediv__"
|
||||
| "__rfloordiv__"
|
||||
| "__rmod__"
|
||||
| "__rdivmod__"
|
||||
| "__rpow__"
|
||||
| "__rlshift__"
|
||||
| "__rrshift__"
|
||||
| "__rand__"
|
||||
| "__rxor__"
|
||||
| "__ror__"
|
||||
| "__iadd__"
|
||||
| "__isub__"
|
||||
| "__imul__"
|
||||
| "__imatmul__"
|
||||
| "__itruediv__"
|
||||
| "__ifloordiv__"
|
||||
| "__imod__"
|
||||
| "__ipow__"
|
||||
| "__ilshift__"
|
||||
| "__irshift__"
|
||||
| "__iand__"
|
||||
| "__ixor__"
|
||||
| "__ior__"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
|
||||
// Ignore `super()` calls.
|
||||
if let Some(call_path) = collect_call_path(func) {
|
||||
if matches!(call_path.as_slice(), ["super"]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if let Some(call_path) = collect_call_path(value) {
|
||||
// Ignore `self` and `cls` accesses.
|
||||
if matches!(call_path.as_slice(), ["self" | "cls" | "mcs"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore accesses on class members from _within_ the class.
|
||||
if checker
|
||||
.semantic()
|
||||
.scopes
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|scope| match &scope.kind {
|
||||
ScopeKind::Class(ast::StmtClassDef { name, .. }) => Some(name),
|
||||
_ => None,
|
||||
})
|
||||
.is_some_and(|name| {
|
||||
if call_path.as_slice() == [name.as_str()] {
|
||||
checker
|
||||
.semantic()
|
||||
.find_binding(name)
|
||||
.is_some_and(|binding| {
|
||||
// TODO(charlie): Could the name ever be bound to a
|
||||
// _different_ class here?
|
||||
binding.kind.is_class_definition()
|
||||
})
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
PrivateMemberAccess {
|
||||
access: attr.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
PrivateMemberAccess {
|
||||
access: attr.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ impl AlwaysAutofixableViolation for ExprAndNotExpr {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// x and not x
|
||||
/// x or not x
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -115,7 +115,7 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex
|
||||
return;
|
||||
};
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(env_var),
|
||||
value: Constant::Str(ast::StringConstant { value: env_var, .. }),
|
||||
..
|
||||
}) = arg
|
||||
else {
|
||||
@@ -167,7 +167,7 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
|
||||
return;
|
||||
}
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(env_var),
|
||||
value: Constant::Str(ast::StringConstant { value: env_var, .. }),
|
||||
kind,
|
||||
range: _,
|
||||
}) = slice.as_ref()
|
||||
|
||||
@@ -264,15 +264,13 @@ fn is_main_check(expr: &Expr) -> bool {
|
||||
{
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() {
|
||||
if id == "__name__" {
|
||||
if comparators.len() == 1 {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
}) = &comparators[0]
|
||||
{
|
||||
if value == "__main__" {
|
||||
return true;
|
||||
}
|
||||
if let [Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(ast::StringConstant { value, .. }),
|
||||
..
|
||||
})] = comparators.as_slice()
|
||||
{
|
||||
if value == "__main__" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
75
crates/ruff/src/rules/flake8_tidy_imports/matchers.rs
Normal file
75
crates/ruff/src/rules/flake8_tidy_imports/matchers.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
/// Match an imported member against the ban policy. For example, given `from foo import bar`,
|
||||
/// `foo` is the module and `bar` is the member. Performs an exact match.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MatchName<'a> {
|
||||
pub(crate) module: &'a str,
|
||||
pub(crate) member: &'a str,
|
||||
}
|
||||
|
||||
impl MatchName<'_> {
|
||||
fn is_match(&self, banned_module: &str) -> bool {
|
||||
// Ex) Match banned `foo.bar` to import `foo.bar`, without allocating, assuming that
|
||||
// `module` is `foo`, `member` is `bar`, and `banned_module` is `foo.bar`.
|
||||
banned_module
|
||||
.strip_prefix(self.module)
|
||||
.and_then(|banned_module| banned_module.strip_prefix('.'))
|
||||
.and_then(|banned_module| banned_module.strip_prefix(self.member))
|
||||
.is_some_and(str::is_empty)
|
||||
}
|
||||
}
|
||||
|
||||
/// Match an imported module against the ban policy. For example, given `import foo.bar`,
|
||||
/// `foo.bar` is the module. Matches against the module name or any of its parents.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MatchNameOrParent<'a> {
|
||||
pub(crate) module: &'a str,
|
||||
}
|
||||
|
||||
impl MatchNameOrParent<'_> {
|
||||
fn is_match(&self, banned_module: &str) -> bool {
|
||||
// Ex) Match banned `foo` to import `foo`.
|
||||
if self.module == banned_module {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ex) Match banned `foo` to import `foo.bar`.
|
||||
if self
|
||||
.module
|
||||
.strip_prefix(banned_module)
|
||||
.is_some_and(|suffix| suffix.starts_with('.'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum NameMatchPolicy<'a> {
|
||||
/// Only match an exact module name (e.g., given `import foo.bar`, only match `foo.bar`).
|
||||
MatchName(MatchName<'a>),
|
||||
/// Match an exact module name or any of its parents (e.g., given `import foo.bar`, match
|
||||
/// `foo.bar` or `foo`).
|
||||
MatchNameOrParent(MatchNameOrParent<'a>),
|
||||
}
|
||||
|
||||
impl NameMatchPolicy<'_> {
|
||||
pub(crate) fn find<'a>(&self, banned_modules: impl Iterator<Item = &'a str>) -> Option<String> {
|
||||
for banned_module in banned_modules {
|
||||
match self {
|
||||
NameMatchPolicy::MatchName(matcher) => {
|
||||
if matcher.is_match(banned_module) {
|
||||
return Some(banned_module.to_string());
|
||||
}
|
||||
}
|
||||
NameMatchPolicy::MatchNameOrParent(matcher) => {
|
||||
if matcher.is_match(banned_module) {
|
||||
return Some(banned_module.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Rules from [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/).
|
||||
pub(crate) mod matchers;
|
||||
pub mod options;
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
@@ -124,4 +125,23 @@ mod tests {
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banned_module_level_imports() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_tidy_imports/TID253.py"),
|
||||
&Settings {
|
||||
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
|
||||
banned_module_level_imports: vec![
|
||||
"torch".to_string(),
|
||||
"tensorflow".to_string(),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
..Settings::for_rules(vec![Rule::BannedModuleLevelImports])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user