Merge branch 'main' into 5246_try301_identical_rule

This commit is contained in:
Charlie Marsh 2023-07-04 14:45:10 -04:00
commit f244d0387a
302 changed files with 9241 additions and 2780 deletions

1
.gitattributes vendored
View File

@ -4,3 +4,4 @@ crates/ruff/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf
crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf
ruff.schema.json linguist-generated=true text=auto eol=lf
*.md.snap linguist-language=Markdown

View File

@ -1,5 +1,31 @@
# Breaking Changes
## 0.0.276
### The `keep-runtime-typing` setting has been reinstated ([#5470](https://github.com/astral-sh/ruff/pull/5470))
The `keep-runtime-typing` setting has been reinstated with revised semantics. This setting was
removed in [#4427](https://github.com/astral-sh/ruff/pull/4427), as it was equivalent to ignoring
the `UP006` and `UP007` rules via Ruff's standard `ignore` mechanism.
Taking `UP006` (rewrite `List[int]` to `list[int]`) as an example, the setting now behaves as
follows:
- On Python 3.7 and Python 3.8, setting `keep-runtime-typing = true` will cause Ruff to ignore
`UP006` violations, even if `from __future__ import annotations` is present in the file.
While such annotations are valid in Python 3.7 and Python 3.8 when combined with
`from __future__ import annotations`, they aren't supported by libraries like Pydantic and
FastAPI, which rely on runtime type checking.
- On Python 3.9 and above, the setting has no effect, as `list[int]` is a valid type annotation,
and libraries like Pydantic and FastAPI support it without issue.
In short: `keep-runtime-typing` can be used to ensure that Ruff doesn't introduce type annotations
that are not supported at runtime by the current Python version, which are unsupported by libraries
like Pydantic and FastAPI.
Note that this is not a breaking change, but is included here to complement the previous removal
of `keep-runtime-typing`.
## 0.0.268
### The `keep-runtime-typing` setting has been removed ([#4427](https://github.com/astral-sh/ruff/pull/4427))

7
Cargo.lock generated
View File

@ -746,7 +746,7 @@ dependencies = [
[[package]]
name = "flake8-to-ruff"
version = "0.0.275"
version = "0.0.276"
dependencies = [
"anyhow",
"clap",
@ -1829,7 +1829,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.275"
version = "0.0.276"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@ -1865,6 +1865,7 @@ dependencies = [
"result-like",
"ruff_cache",
"ruff_diagnostics",
"ruff_index",
"ruff_macros",
"ruff_python_ast",
"ruff_python_semantic",
@ -1926,7 +1927,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.275"
version = "0.0.276"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",

View File

@ -139,7 +139,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.275
rev: v0.0.276
hooks:
- id: ruff
```

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.275"
version = "0.0.276"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@ -17,6 +17,7 @@ name = "ruff"
[dependencies]
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_index = { path = "../ruff_index" }
ruff_macros = { path = "../ruff_macros" }
ruff_python_whitespace = { path = "../ruff_python_whitespace" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
@ -88,3 +89,5 @@ colored = { workspace = true, features = ["no-color"] }
[features]
default = []
schemars = ["dep:schemars"]
# Enables the UnreachableCode rule
unreachable-code = []

View File

@ -0,0 +1,11 @@
def func():
assert True
def func():
assert False
def func():
assert True, "oops"
def func():
assert False, "oops"

View File

@ -0,0 +1,41 @@
def func():
async for i in range(5):
print(i)
def func():
async for i in range(20):
print(i)
else:
return 0
def func():
async for i in range(10):
if i == 5:
return 1
return 0
def func():
async for i in range(111):
if i == 5:
return 1
else:
return 0
return 2
def func():
async for i in range(12):
continue
def func():
async for i in range(1110):
if True:
continue
def func():
async for i in range(13):
break
def func():
async for i in range(1110):
if True:
break

View File

@ -0,0 +1,41 @@
def func():
for i in range(5):
print(i)
def func():
for i in range(20):
print(i)
else:
return 0
def func():
for i in range(10):
if i == 5:
return 1
return 0
def func():
for i in range(111):
if i == 5:
return 1
else:
return 0
return 2
def func():
for i in range(12):
continue
def func():
for i in range(1110):
if True:
continue
def func():
for i in range(13):
break
def func():
for i in range(1110):
if True:
break

View File

@ -0,0 +1,108 @@
def func():
if False:
return 0
return 1
def func():
if True:
return 1
return 0
def func():
if False:
return 0
else:
return 1
def func():
if True:
return 1
else:
return 0
def func():
if False:
return 0
else:
return 1
return "unreachable"
def func():
if True:
return 1
else:
return 0
return "unreachable"
def func():
if True:
if True:
return 1
return 2
else:
return 3
return "unreachable2"
def func():
if False:
return 0
def func():
if True:
return 1
def func():
if True:
return 1
elif False:
return 2
else:
return 0
def func():
if False:
return 1
elif True:
return 2
else:
return 0
def func():
if True:
if False:
return 0
elif True:
return 1
else:
return 2
return 3
elif True:
return 4
else:
return 5
return 6
def func():
if False:
return "unreached"
elif False:
return "also unreached"
return "reached"
# Test case found in the Bokeh repository that trigger a false positive.
def func(self, obj: BytesRep) -> bytes:
data = obj["data"]
if isinstance(data, str):
return base64.b64decode(data)
elif isinstance(data, Buffer):
buffer = data
else:
id = data["id"]
if id in self._buffers:
buffer = self._buffers[id]
else:
self.error(f"can't resolve buffer '{id}'")
return buffer.data

View File

@ -0,0 +1,131 @@
def func(status):
match status:
case _:
return 0
return "unreachable"
def func(status):
match status:
case 1:
return 1
return 0
def func(status):
match status:
case 1:
return 1
case _:
return 0
def func(status):
match status:
case 1 | 2 | 3:
return 5
return 6
def func(status):
match status:
case 1 | 2 | 3:
return 5
case _:
return 10
return 0
def func(status):
match status:
case 0:
return 0
case 1:
return 1
case 1:
return "1 again"
case _:
return 3
def func(status):
i = 0
match status, i:
case _, _:
return 0
def func(status):
i = 0
match status, i:
case _, 0:
return 0
case _, 2:
return 0
def func(point):
match point:
case (0, 0):
print("Origin")
case _:
raise ValueError("oops")
def func(point):
match point:
case (0, 0):
print("Origin")
case (0, y):
print(f"Y={y}")
case (x, 0):
print(f"X={x}")
case (x, y):
print(f"X={x}, Y={y}")
case _:
raise ValueError("Not a point")
def where_is(point):
class Point:
x: int
y: int
match point:
case Point(x=0, y=0):
print("Origin")
case Point(x=0, y=y):
print(f"Y={y}")
case Point(x=x, y=0):
print(f"X={x}")
case Point():
print("Somewhere else")
case _:
print("Not a point")
def func(points):
match points:
case []:
print("No points")
case [Point(0, 0)]:
print("The origin")
case [Point(x, y)]:
print(f"Single point {x}, {y}")
case [Point(0, y1), Point(0, y2)]:
print(f"Two on the Y axis at {y1}, {y2}")
case _:
print("Something else")
def func(point):
match point:
case Point(x, y) if x == y:
print(f"Y=X at {x}")
case Point(x, y):
print(f"Not on the diagonal")
def func():
from enum import Enum
class Color(Enum):
RED = 'red'
GREEN = 'green'
BLUE = 'blue'
color = Color(input("Enter your choice of 'red', 'blue' or 'green': "))
match color:
case Color.RED:
print("I see red!")
case Color.GREEN:
print("Grass is green")
case Color.BLUE:
print("I'm feeling the blues :(")

View File

@ -0,0 +1,5 @@
def func():
raise Exception
def func():
raise "a glass!"

View File

@ -0,0 +1,23 @@
def func():
pass
def func():
pass
def func():
return
def func():
return 1
def func():
return 1
return "unreachable"
def func():
i = 0
def func():
i = 0
i += 2
return i

View File

@ -0,0 +1,41 @@
def func():
try:
...
except Exception:
...
except OtherException as e:
...
else:
...
finally:
...
def func():
try:
...
except Exception:
...
def func():
try:
...
except Exception:
...
except OtherException as e:
...
def func():
try:
...
except Exception:
...
except OtherException as e:
...
else:
...
def func():
try:
...
finally:
...

View File

@ -0,0 +1,121 @@
def func():
while False:
return "unreachable"
return 1
def func():
while False:
return "unreachable"
else:
return 1
def func():
while False:
return "unreachable"
else:
return 1
return "also unreachable"
def func():
while True:
return 1
return "unreachable"
def func():
while True:
return 1
else:
return "unreachable"
def func():
while True:
return 1
else:
return "unreachable"
return "also unreachable"
def func():
i = 0
while False:
i += 1
return i
def func():
i = 0
while True:
i += 1
return i
def func():
while True:
pass
return 1
def func():
i = 0
while True:
if True:
print("ok")
i += 1
return i
def func():
i = 0
while True:
if False:
print("ok")
i += 1
return i
def func():
while True:
if True:
return 1
return 0
def func():
while True:
continue
def func():
while False:
continue
def func():
while True:
break
def func():
while False:
break
def func():
while True:
if True:
continue
def func():
while True:
if True:
break
'''
TODO: because `try` statements aren't handled this triggers a false positive as
the last statement is reached, but the rules thinks it isn't (it doesn't
see/process the break statement).
# Test case found in the Bokeh repository that trigger a false positive.
def bokeh2(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None:
self.stop_serving = False
while True:
try:
self.server = HTTPServer((host, port), HtmlOnlyHandler)
self.host = host
self.port = port
break
except OSError:
log.debug(f"port {port} is in use, trying to next one")
port += 1
self.thread = threading.Thread(target=self._run_web_server)
'''

View File

@ -0,0 +1,12 @@
import os
print(eval("1+1")) # S307
print(eval("os.getcwd()")) # S307
class Class(object):
def eval(self):
print("hi")
def foo(self):
self.eval() # OK

View File

@ -23,6 +23,10 @@ class Foobar(unittest.TestCase):
with self.assertRaises(Exception):
raise Exception("Evil I say!")
def also_evil_raises(self) -> None:
with self.assertRaises(BaseException):
raise Exception("Evil I say!")
def context_manager_raises(self) -> None:
with self.assertRaises(Exception) as ex:
raise Exception("Context manager is good")
@ -41,6 +45,9 @@ def test_pytest_raises():
with pytest.raises(Exception):
raise ValueError("Hello")
with pytest.raises(Exception), pytest.raises(ValueError):
raise ValueError("Hello")
with pytest.raises(Exception, "hello"):
raise ValueError("This is fine")

View File

@ -111,3 +111,19 @@ class PerfectlyFine(models.Model):
@property
def random_property(self):
return "%s" % self
class MultipleConsecutiveFields(models.Model):
"""Model that contains multiple out-of-order field definitions in a row."""
class Meta:
verbose_name = "test"
first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
def get_absolute_url(self):
pass
middle_name = models.CharField(max_length=32)

View File

@ -0,0 +1,6 @@
import sys
if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@ -0,0 +1,6 @@
import sys
if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@ -0,0 +1,31 @@
import sys
if sys.version_info[0] == 2: ...
if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:'
if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check
if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2,): ...
if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple
if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check
if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check
if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version
if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons
if sys.version_info > (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info < (3, 5): ...
if sys.version_info >= (3, 5): ...
if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@ -0,0 +1,31 @@
import sys
if sys.version_info[0] == 2: ...
if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:'
if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check
if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2,): ...
if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple
if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check
if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check
if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version
if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons
if sys.version_info > (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info < (3, 5): ...
if sys.version_info >= (3, 5): ...
if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@ -0,0 +1,15 @@
import sys
from sys import version_info
if sys.version_info >= (3, 4, 3): ... # PYI004
if sys.version_info < (3, 4, 3): ... # PYI004
if sys.version_info == (3, 4, 3): ... # PYI004
if sys.version_info != (3, 4, 3): ... # PYI004
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if sys.platform == 'linux': ...

View File

@ -0,0 +1,15 @@
import sys
from sys import version_info
if sys.version_info >= (3, 4, 3): ... # PYI004
if sys.version_info < (3, 4, 3): ... # PYI004
if sys.version_info == (3, 4, 3): ... # PYI004
if sys.version_info != (3, 4, 3): ... # PYI004
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if sys.platform == 'linux': ...

View File

@ -0,0 +1,14 @@
import sys
from sys import platform, version_info
if sys.version_info[:1] == (2, 7): ... # Y005
if sys.version_info[:2] == (2,): ... # Y005
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if platform == 'linux': ...

View File

@ -0,0 +1,14 @@
import sys
from sys import platform, version_info
if sys.version_info[:1] == (2, 7): ... # Y005
if sys.version_info[:2] == (2,): ... # Y005
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if platform == 'linux': ...

View File

@ -91,3 +91,4 @@ field27 = list[str]
field28 = builtins.str
field29 = str
field30 = str | bytes | None
field31: typing.Final = field30

View File

@ -98,3 +98,4 @@ field27 = list[str]
field28 = builtins.str
field29 = str
field30 = str | bytes | None
field31: typing.Final = field30

View File

@ -1,5 +1,6 @@
# Do this (new version)
from numpy.random import default_rng
rng = default_rng()
vals = rng.standard_normal(10)
more_vals = rng.standard_normal(10)
@ -7,11 +8,13 @@ numbers = rng.integers(high, size=5)
# instead of this (legacy version)
from numpy import random
vals = random.standard_normal(10)
more_vals = random.standard_normal(10)
numbers = random.integers(high, size=5)
import numpy
numpy.random.seed()
numpy.random.get_state()
numpy.random.set_state()

View File

@ -0,0 +1,15 @@
import numpy as np
np.round_(np.random.rand(5, 5), 2)
np.product(np.random.rand(5, 5))
np.cumproduct(np.random.rand(5, 5))
np.sometrue(np.random.rand(5, 5))
np.alltrue(np.random.rand(5, 5))
from numpy import round_, product, cumproduct, sometrue, alltrue
round_(np.random.rand(5, 5), 2)
product(np.random.rand(5, 5))
cumproduct(np.random.rand(5, 5))
sometrue(np.random.rand(5, 5))
alltrue(np.random.rand(5, 5))

View File

@ -4,7 +4,9 @@ x = pd.DataFrame()
x.drop(["a"], axis=1, inplace=True)
x.drop(["a"], axis=1, inplace=True)
x.y.drop(["a"], axis=1, inplace=True)
x["y"].drop(["a"], axis=1, inplace=True)
x.drop(
inplace=True,
@ -23,6 +25,7 @@ x.drop(["a"], axis=1, **kwargs, inplace=True)
x.drop(["a"], axis=1, inplace=True, **kwargs)
f(x.drop(["a"], axis=1, inplace=True))
x.apply(lambda x: x.sort_values('a', inplace=True))
x.apply(lambda x: x.sort_values("a", inplace=True))
import torch
torch.m.ReLU(inplace=True) # safe because this isn't a pandas call

View File

@ -0,0 +1,32 @@
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
if i % 2:
result.append(i) # PERF401
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i * i) # PERF401
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
if i % 2:
result.append(i) # PERF401
elif i % 2:
result.append(i) # PERF401
else:
result.append(i) # PERF401
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i) # OK

View File

@ -0,0 +1,19 @@
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i) # PERF402
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.insert(0, i) # PERF402
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i * i) # OK

View File

@ -0,0 +1,25 @@
def f(a: int, b: int) -> int:
"""Showcase function.
Parameters
----------
a : int
_description_
b : int
_description_
Returns
-------
int
_description
"""
return b - a
def f() -> int:
"""Showcase function.
Parameters
----------
Returns
-------
"""

View File

@ -70,3 +70,8 @@ print("foo".encode()) # print(b"foo")
"abc"
"def"
)).encode()
(f"foo{bar}").encode("utf-8")
(f"foo{bar}").encode(encoding="utf-8")
("unicode text©").encode("utf-8")
("unicode text©").encode(encoding="utf-8")

View File

@ -0,0 +1,185 @@
def after_return():
return "reachable"
return "unreachable"
async def also_works_on_async_functions():
return "reachable"
return "unreachable"
def if_always_true():
if True:
return "reachable"
return "unreachable"
def if_always_false():
if False:
return "unreachable"
return "reachable"
def if_elif_always_false():
if False:
return "unreachable"
elif False:
return "also unreachable"
return "reachable"
def if_elif_always_true():
if False:
return "unreachable"
elif True:
return "reachable"
return "also unreachable"
def ends_with_if():
if False:
return "unreachable"
else:
return "reachable"
def infinite_loop():
while True:
continue
return "unreachable"
''' TODO: we could determine these, but we don't yet.
def for_range_return():
for i in range(10):
if i == 5:
return "reachable"
return "unreachable"
def for_range_else():
for i in range(111):
if i == 5:
return "reachable"
else:
return "unreachable"
return "also unreachable"
def for_range_break():
for i in range(13):
return "reachable"
return "unreachable"
def for_range_if_break():
for i in range(1110):
if True:
return "reachable"
return "unreachable"
'''
def match_wildcard(status):
match status:
case _:
return "reachable"
return "unreachable"
def match_case_and_wildcard(status):
match status:
case 1:
return "reachable"
case _:
return "reachable"
return "unreachable"
def raise_exception():
raise Exception
return "unreachable"
def while_false():
while False:
return "unreachable"
return "reachable"
def while_false_else():
while False:
return "unreachable"
else:
return "reachable"
def while_false_else_return():
while False:
return "unreachable"
else:
return "reachable"
return "also unreachable"
def while_true():
while True:
return "reachable"
return "unreachable"
def while_true_else():
while True:
return "reachable"
else:
return "unreachable"
def while_true_else_return():
while True:
return "reachable"
else:
return "unreachable"
return "also unreachable"
def while_false_var_i():
i = 0
while False:
i += 1
return i
def while_true_var_i():
i = 0
while True:
i += 1
return i
def while_infinite():
while True:
pass
return "unreachable"
def while_if_true():
while True:
if True:
return "reachable"
return "unreachable"
# Test case found in the Bokeh repository that trigger a false positive.
def bokeh1(self, obj: BytesRep) -> bytes:
data = obj["data"]
if isinstance(data, str):
return base64.b64decode(data)
elif isinstance(data, Buffer):
buffer = data
else:
id = data["id"]
if id in self._buffers:
buffer = self._buffers[id]
else:
self.error(f"can't resolve buffer '{id}'")
return buffer.data
'''
TODO: because `try` statements aren't handled this triggers a false positive as
the last statement is reached, but the rules thinks it isn't (it doesn't
see/process the break statement).
# Test case found in the Bokeh repository that trigger a false positive.
def bokeh2(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None:
self.stop_serving = False
while True:
try:
self.server = HTTPServer((host, port), HtmlOnlyHandler)
self.host = host
self.port = port
break
except OSError:
log.debug(f"port {port} is in use, trying to next one")
port += 1
self.thread = threading.Thread(target=self._run_web_server)
'''

View File

@ -36,6 +36,7 @@ use crate::importer::Importer;
use crate::noqa::NoqaMapping;
use crate::registry::Rule;
use crate::rules::flake8_builtins::helpers::AnyShadowing;
use crate::rules::{
airflow, flake8_2020, flake8_annotations, flake8_async, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez,
@ -71,7 +72,7 @@ pub(crate) struct Checker<'a> {
deferred: Deferred<'a>,
pub(crate) diagnostics: Vec<Diagnostic>,
// Check-specific state.
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
pub(crate) flake8_bugbear_seen: Vec<&'a ast::ExprName>,
}
impl<'a> Checker<'a> {
@ -358,11 +359,7 @@ where
..
}) => {
if self.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
self.diagnostics
.extend(flake8_django::rules::non_leading_receiver_decorator(
decorator_list,
|expr| self.semantic.resolve_call_path(expr),
));
flake8_django::rules::non_leading_receiver_decorator(self, decorator_list);
}
if self.enabled(Rule::AmbiguousFunctionName) {
if let Some(diagnostic) =
@ -504,8 +501,7 @@ where
}
}
if self.enabled(Rule::HardcodedPasswordDefault) {
self.diagnostics
.extend(flake8_bandit::rules::hardcoded_password_default(args));
flake8_bandit::rules::hardcoded_password_default(self, args);
}
if self.enabled(Rule::PropertyWithParameters) {
pylint::rules::property_with_parameters(self, stmt, decorator_list, args);
@ -623,6 +619,11 @@ where
);
}
}
#[cfg(feature = "unreachable-code")]
if self.enabled(Rule::UnreachableCode) {
self.diagnostics
.extend(ruff::rules::unreachable::in_function(name, body));
}
}
Stmt::Return(_) => {
if self.enabled(Rule::ReturnOutsideFunction) {
@ -643,10 +644,7 @@ where
},
) => {
if self.enabled(Rule::DjangoNullableModelStringField) {
self.diagnostics
.extend(flake8_django::rules::nullable_model_string_field(
self, body,
));
flake8_django::rules::nullable_model_string_field(self, body);
}
if self.enabled(Rule::DjangoExcludeWithModelForm) {
if let Some(diagnostic) =
@ -667,21 +665,17 @@ where
}
if !self.is_stub {
if self.enabled(Rule::DjangoModelWithoutDunderStr) {
if let Some(diagnostic) =
flake8_django::rules::model_without_dunder_str(self, bases, body, stmt)
{
self.diagnostics.push(diagnostic);
}
flake8_django::rules::model_without_dunder_str(self, class_def);
}
}
if self.enabled(Rule::GlobalStatement) {
pylint::rules::global_statement(self, name);
}
if self.enabled(Rule::UselessObjectInheritance) {
pyupgrade::rules::useless_object_inheritance(self, class_def, stmt);
pyupgrade::rules::useless_object_inheritance(self, class_def);
}
if self.enabled(Rule::UnnecessaryClassParentheses) {
pyupgrade::rules::unnecessary_class_parentheses(self, class_def, stmt);
pyupgrade::rules::unnecessary_class_parentheses(self, class_def);
}
if self.enabled(Rule::AmbiguousClassName) {
if let Some(diagnostic) =
@ -1370,6 +1364,51 @@ where
self.diagnostics.push(diagnostic);
}
}
if self.is_stub {
if self.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck,
Rule::PatchVersionComparison,
Rule::WrongTupleLengthVersionComparison,
]) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::unrecognized_version_info(self, value);
}
} else {
flake8_pyi::rules::unrecognized_version_info(self, test);
}
}
if self.any_enabled(&[
Rule::UnrecognizedPlatformCheck,
Rule::UnrecognizedPlatformName,
]) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::unrecognized_platform(self, value);
}
} else {
flake8_pyi::rules::unrecognized_platform(self, test);
}
}
if self.enabled(Rule::BadVersionInfoComparison) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::bad_version_info_comparison(self, value);
}
} else {
flake8_pyi::rules::bad_version_info_comparison(self, test);
}
}
if self.enabled(Rule::ComplexIfStatementInStub) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::complex_if_statement_in_stub(self, value);
}
} else {
flake8_pyi::rules::complex_if_statement_in_stub(self, test);
}
}
}
}
Stmt::Assert(ast::StmtAssert {
test,
@ -1409,7 +1448,7 @@ where
Stmt::With(ast::StmtWith { items, body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => {
if self.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(self, stmt, items);
flake8_bugbear::rules::assert_raises_exception(self, items);
}
if self.enabled(Rule::PytestRaisesWithMultipleStatements) {
flake8_pytest_style::rules::complex_raises(self, stmt, items, body);
@ -1490,6 +1529,12 @@ where
if self.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(self, target, iter);
}
if self.enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(self, target, body);
}
if self.enabled(Rule::ManualListCopy) {
perflint::rules::manual_list_copy(self, target, body);
}
if self.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(self, iter);
}
@ -1528,9 +1573,7 @@ where
pyupgrade::rules::os_error_alias_handlers(self, handlers);
}
if self.enabled(Rule::PytestAssertInExcept) {
self.diagnostics.extend(
flake8_pytest_style::rules::assert_in_exception_handler(handlers),
);
flake8_pytest_style::rules::assert_in_exception_handler(self, handlers);
}
if self.enabled(Rule::SuppressibleException) {
flake8_simplify::rules::suppressible_exception(
@ -1571,11 +1614,7 @@ where
flake8_bugbear::rules::assignment_to_os_environ(self, targets);
}
if self.enabled(Rule::HardcodedPasswordString) {
if let Some(diagnostic) =
flake8_bandit::rules::assign_hardcoded_password_string(value, targets)
{
self.diagnostics.push(diagnostic);
}
flake8_bandit::rules::assign_hardcoded_password_string(self, value, targets);
}
if self.enabled(Rule::GlobalStatement) {
for target in targets.iter() {
@ -2107,6 +2146,7 @@ where
&& self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{
flake8_future_annotations::rules::future_rewritable_type_annotation(
self, value,
@ -2117,7 +2157,8 @@ where
if self.settings.target_version >= PythonVersion::Py310
|| (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations()
&& self.semantic.in_annotation())
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
{
pyupgrade::rules::use_pep604_annotation(
self, expr, slice, operator,
@ -2193,6 +2234,9 @@ where
if self.enabled(Rule::NumpyDeprecatedTypeAlias) {
numpy::rules::deprecated_type_alias(self, expr);
}
if self.enabled(Rule::NumpyDeprecatedFunction) {
numpy::rules::deprecated_function(self, expr);
}
if self.is_stub {
if self.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(self, expr);
@ -2212,6 +2256,7 @@ where
&& self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{
flake8_future_annotations::rules::future_rewritable_type_annotation(
self, expr,
@ -2222,7 +2267,8 @@ where
if self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations()
&& self.semantic.in_annotation())
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
{
pyupgrade::rules::use_pep585_annotation(
self,
@ -2287,6 +2333,7 @@ where
&& self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{
flake8_future_annotations::rules::future_rewritable_type_annotation(
self, expr,
@ -2297,7 +2344,8 @@ where
if self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations()
&& self.semantic.in_annotation())
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
{
pyupgrade::rules::use_pep585_annotation(self, expr, &replacement);
}
@ -2315,6 +2363,9 @@ where
if self.enabled(Rule::NumpyDeprecatedTypeAlias) {
numpy::rules::deprecated_type_alias(self, expr);
}
if self.enabled(Rule::NumpyDeprecatedFunction) {
numpy::rules::deprecated_function(self, expr);
}
if self.enabled(Rule::DeprecatedMockImport) {
pyupgrade::rules::deprecated_mock_attribute(self, expr);
}
@ -2533,9 +2584,7 @@ where
flake8_pie::rules::unnecessary_dict_kwargs(self, expr, keywords);
}
if self.enabled(Rule::ExecBuiltin) {
if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) {
self.diagnostics.push(diagnostic);
}
flake8_bandit::rules::exec_used(self, func);
}
if self.enabled(Rule::BadFilePermissions) {
flake8_bandit::rules::bad_file_permissions(self, func, args, keywords);
@ -2558,8 +2607,7 @@ where
flake8_bandit::rules::jinja2_autoescape_false(self, func, args, keywords);
}
if self.enabled(Rule::HardcodedPasswordFuncArg) {
self.diagnostics
.extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords));
flake8_bandit::rules::hardcoded_password_func_arg(self, keywords);
}
if self.enabled(Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
@ -2692,17 +2740,12 @@ where
flake8_debugger::rules::debugger_call(self, expr, func);
}
if self.enabled(Rule::PandasUseOfInplaceArgument) {
self.diagnostics.extend(
pandas_vet::rules::inplace_argument(self, expr, func, args, keywords)
.into_iter(),
);
pandas_vet::rules::inplace_argument(self, expr, func, args, keywords);
}
pandas_vet::rules::call(self, func);
if self.enabled(Rule::PandasUseOfPdMerge) {
if let Some(diagnostic) = pandas_vet::rules::use_of_pd_merge(func) {
self.diagnostics.push(diagnostic);
};
pandas_vet::rules::use_of_pd_merge(self, func);
}
if self.enabled(Rule::CallDatetimeWithoutTzinfo) {
flake8_datetimez::rules::call_datetime_without_tzinfo(
@ -2819,16 +2862,13 @@ where
&self.settings.flake8_gettext.functions_names,
) {
if self.enabled(Rule::FStringInGetTextFuncCall) {
self.diagnostics
.extend(flake8_gettext::rules::f_string_in_gettext_func_call(args));
flake8_gettext::rules::f_string_in_gettext_func_call(self, args);
}
if self.enabled(Rule::FormatInGetTextFuncCall) {
self.diagnostics
.extend(flake8_gettext::rules::format_in_gettext_func_call(args));
flake8_gettext::rules::format_in_gettext_func_call(self, args);
}
if self.enabled(Rule::PrintfInGetTextFuncCall) {
self.diagnostics
.extend(flake8_gettext::rules::printf_in_gettext_func_call(args));
flake8_gettext::rules::printf_in_gettext_func_call(self, args);
}
}
if self.enabled(Rule::UncapitalizedEnvironmentVariables) {
@ -2869,7 +2909,7 @@ where
flake8_use_pathlib::rules::replaceable_by_pathlib(self, func);
}
if self.enabled(Rule::NumpyLegacyRandom) {
numpy::rules::numpy_legacy_random(self, func);
numpy::rules::legacy_random(self, func);
}
if self.any_enabled(&[
Rule::LoggingStringFormat,
@ -3169,11 +3209,10 @@ where
flake8_2020::rules::compare(self, left, ops, comparators);
}
if self.enabled(Rule::HardcodedPasswordString) {
self.diagnostics.extend(
flake8_bandit::rules::compare_to_hardcoded_password_string(
self,
left,
comparators,
),
);
}
if self.enabled(Rule::ComparisonWithItself) {
@ -3194,29 +3233,6 @@ where
if self.enabled(Rule::YodaConditions) {
flake8_simplify::rules::yoda_conditions(self, expr, left, ops, comparators);
}
if self.is_stub {
if self.any_enabled(&[
Rule::UnrecognizedPlatformCheck,
Rule::UnrecognizedPlatformName,
]) {
flake8_pyi::rules::unrecognized_platform(
self,
expr,
left,
ops,
comparators,
);
}
if self.enabled(Rule::BadVersionInfoComparison) {
flake8_pyi::rules::bad_version_info_comparison(
self,
expr,
left,
ops,
comparators,
);
}
}
}
Expr::Constant(ast::ExprConstant {
value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. },
@ -4422,7 +4438,7 @@ impl<'a> Checker<'a> {
}
fn handle_node_delete(&mut self, expr: &'a Expr) {
let Expr::Name(ast::ExprName { id, .. } )= expr else {
let Expr::Name(ast::ExprName { id, .. }) = expr else {
return;
};

View File

@ -596,6 +596,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-pyi
(Flake8Pyi, "001") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnprefixedTypeParam),
(Flake8Pyi, "002") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ComplexIfStatementInStub),
(Flake8Pyi, "003") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedVersionInfoCheck),
(Flake8Pyi, "004") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::PatchVersionComparison),
(Flake8Pyi, "005") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::WrongTupleLengthVersionComparison),
(Flake8Pyi, "006") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadVersionInfoComparison),
(Flake8Pyi, "007") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformCheck),
(Flake8Pyi, "008") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformName),
@ -742,6 +746,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// numpy
(Numpy, "001") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedTypeAlias),
(Numpy, "002") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyLegacyRandom),
(Numpy, "003") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedFunction),
// ruff
(Ruff, "001") => (RuleGroup::Unspecified, rules::ruff::rules::AmbiguousUnicodeCharacterString),
@ -756,6 +761,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "011") => (RuleGroup::Unspecified, rules::ruff::rules::StaticKeyDictComprehension),
(Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault),
(Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional),
#[cfg(feature = "unreachable-code")]
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),
@ -788,6 +795,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Perflint, "101") => (RuleGroup::Unspecified, rules::perflint::rules::UnnecessaryListCast),
(Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator),
(Perflint, "203") => (RuleGroup::Unspecified, rules::perflint::rules::TryExceptInLoop),
(Perflint, "401") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListComprehension),
(Perflint, "402") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListCopy),
// flake8-fixme
(Flake8Fixme, "001") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsFixme),

View File

@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
use ruff_python_whitespace::{UniversalNewlineIterator, UniversalNewlines};
use ruff_python_whitespace::{Line, UniversalNewlineIterator, UniversalNewlines};
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::{Docstring, DocstringBody};
@ -144,15 +144,13 @@ impl<'a> SectionContexts<'a> {
let mut contexts = Vec::new();
let mut last: Option<SectionContextData> = None;
let mut previous_line = None;
for line in contents.universal_newlines() {
if previous_line.is_none() {
// skip the first line
previous_line = Some(line.as_str());
continue;
}
let mut lines = contents.universal_newlines().peekable();
// Skip the first line, which is the summary.
let mut previous_line = lines.next();
while let Some(line) = lines.next() {
if let Some(section_kind) = suspected_as_section(&line, style) {
let indent = leading_space(&line);
let section_name = leading_words(&line);
@ -162,7 +160,8 @@ impl<'a> SectionContexts<'a> {
if is_docstring_section(
&line,
section_name_range,
previous_line.unwrap_or_default(),
previous_line.as_ref(),
lines.peek(),
) {
if let Some(mut last) = last.take() {
last.range = TextRange::new(last.range.start(), line.start());
@ -178,7 +177,7 @@ impl<'a> SectionContexts<'a> {
}
}
previous_line = Some(line.as_str());
previous_line = Some(line);
}
if let Some(mut last) = last.take() {
@ -388,7 +387,13 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
}
/// Check if the suspected context is really a section header.
fn is_docstring_section(line: &str, section_name_range: TextRange, previous_lines: &str) -> bool {
fn is_docstring_section(
line: &Line,
section_name_range: TextRange,
previous_line: Option<&Line>,
next_line: Option<&Line>,
) -> bool {
// Determine whether the current line looks like a section header, e.g., "Args:".
let section_name_suffix = line[usize::from(section_name_range.end())..].trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
@ -396,13 +401,29 @@ fn is_docstring_section(line: &str, section_name_range: TextRange, previous_line
return false;
}
let prev_line = previous_lines.trim();
let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
// Determine whether the next line is an underline, e.g., "-----".
let next_line_is_underline = next_line.map_or(false, |next_line| {
let next_line = next_line.trim();
if next_line.is_empty() {
false
} else {
let next_line_is_underline = next_line.chars().all(|char| matches!(char, '-' | '='));
next_line_is_underline
}
});
if next_line_is_underline {
return true;
}
// Determine whether the previous line looks like the end of a paragraph.
let previous_line_looks_like_end_of_paragraph = previous_line.map_or(true, |previous_line| {
let previous_line = previous_line.trim();
let previous_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
.into_iter()
.any(|char| prev_line.ends_with(char));
let prev_line_looks_like_end_of_paragraph =
prev_line_ends_with_punctuation || prev_line.is_empty();
if !prev_line_looks_like_end_of_paragraph {
.any(|char| previous_line.ends_with(char));
previous_line_ends_with_punctuation || previous_line.is_empty()
});
if !previous_line_looks_like_end_of_paragraph {
return false;
}

View File

@ -333,7 +333,7 @@ pub(crate) fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec
for selector in selectors {
if selector
.into_iter()
.any(|rule| Linter::from(plugin).into_iter().any(|r| r == rule))
.any(|rule| Linter::from(plugin).rules().any(|r| r == rule))
{
return true;
}

View File

@ -268,7 +268,8 @@ impl Notebook {
.markers()
.iter()
.rev()
.find(|m| m.source <= *offset) else {
.find(|m| m.source <= *offset)
else {
// There are no markers above the current offset, so we can
// stop here.
break;

View File

@ -19,7 +19,7 @@ impl Rule {
pub fn from_code(code: &str) -> Result<Self, FromCodeError> {
let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?;
let prefix: RuleCodePrefix = RuleCodePrefix::parse(&linter, code)?;
Ok(prefix.into_iter().next().unwrap())
Ok(prefix.rules().next().unwrap())
}
}

View File

@ -3,7 +3,7 @@ use ruff_macros::CacheKey;
use std::fmt::{Debug, Formatter};
use std::iter::FusedIterator;
const RULESET_SIZE: usize = 10;
const RULESET_SIZE: usize = 11;
/// A set of [`Rule`]s.
///

View File

@ -158,16 +158,16 @@ impl IntoIterator for &RuleSelector {
}
RuleSelector::C => RuleSelectorIter::Chain(
Linter::Flake8Comprehensions
.into_iter()
.chain(Linter::McCabe.into_iter()),
.rules()
.chain(Linter::McCabe.rules()),
),
RuleSelector::T => RuleSelectorIter::Chain(
Linter::Flake8Debugger
.into_iter()
.chain(Linter::Flake8Print.into_iter()),
.rules()
.chain(Linter::Flake8Print.rules()),
),
RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.into_iter()),
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.into_iter()),
RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.rules()),
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.clone().rules()),
}
}
}
@ -346,7 +346,7 @@ mod clap_completion {
let prefix = p.linter().common_prefix();
let code = p.short_code();
let mut rules_iter = p.into_iter();
let mut rules_iter = p.rules();
let rule1 = rules_iter.next();
let rule2 = rules_iter.next();

View File

@ -457,11 +457,7 @@ pub(crate) fn definition(
// TODO(charlie): Consider using the AST directly here rather than `Definition`.
// We could adhere more closely to `flake8-annotations` by defining public
// vs. secret vs. protected.
let Definition::Member(Member {
kind,
stmt,
..
}) = definition else {
let Definition::Member(Member { kind, stmt, .. }) = definition else {
return vec![];
};

View File

@ -39,6 +39,7 @@ mod tests {
#[test_case(Rule::SubprocessPopenWithShellEqualsTrue, Path::new("S602.py"))]
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
#[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"))]
#[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"))]
#[test_case(Rule::TryExceptPass, Path::new("S110.py"))]

View File

@ -1,8 +1,10 @@
use rustpython_parser::ast::{self, Expr, Ranged};
use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[violation]
pub struct ExecBuiltin;
@ -14,12 +16,16 @@ impl Violation for ExecBuiltin {
}
/// S102
pub(crate) fn exec_used(expr: &Expr, func: &Expr) -> Option<Diagnostic> {
let Expr::Name(ast::ExprName { id, .. }) = func else {
return None;
};
if id != "exec" {
return None;
pub(crate) fn exec_used(checker: &mut Checker, func: &Expr) {
if checker
.semantic()
.resolve_call_path(func)
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["" | "builtin", "exec"])
})
{
checker
.diagnostics
.push(Diagnostic::new(ExecBuiltin, func.range()));
}
Some(Diagnostic::new(ExecBuiltin, expr.range()))
}

View File

@ -1,5 +1,6 @@
use rustpython_parser::ast::{Arg, ArgWithDefault, Arguments, Expr, Ranged};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@ -36,9 +37,7 @@ fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option<Diagnostic> {
}
/// S107
pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
pub(crate) fn hardcoded_password_default(checker: &mut Checker, arguments: &Arguments) {
for ArgWithDefault {
def,
default,
@ -53,9 +52,7 @@ pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec<Diagnosti
continue;
};
if let Some(diagnostic) = check_password_kwarg(def, default) {
diagnostics.push(diagnostic);
checker.diagnostics.push(diagnostic);
}
}
diagnostics
}

View File

@ -1,5 +1,6 @@
use rustpython_parser::ast::{Keyword, Ranged};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@ -22,10 +23,10 @@ impl Violation for HardcodedPasswordFuncArg {
}
/// S106
pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec<Diagnostic> {
keywords
.iter()
.filter_map(|keyword| {
pub(crate) fn hardcoded_password_func_arg(checker: &mut Checker, keywords: &[Keyword]) {
checker
.diagnostics
.extend(keywords.iter().filter_map(|keyword| {
string_literal(&keyword.value).filter(|string| !string.is_empty())?;
let arg = keyword.arg.as_ref()?;
if !matches_password_name(arg) {
@ -37,6 +38,5 @@ pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec<Diagnosti
},
keyword.range(),
))
})
.collect()
}));
}

View File

@ -3,6 +3,8 @@ use rustpython_parser::ast::{self, Constant, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use super::super::helpers::{matches_password_name, string_literal};
#[violation]
@ -47,12 +49,13 @@ fn password_target(target: &Expr) -> Option<&str> {
/// S105
pub(crate) fn compare_to_hardcoded_password_string(
checker: &mut Checker,
left: &Expr,
comparators: &[Expr],
) -> Vec<Diagnostic> {
comparators
.iter()
.filter_map(|comp| {
) {
checker
.diagnostics
.extend(comparators.iter().filter_map(|comp| {
string_literal(comp).filter(|string| !string.is_empty())?;
let Some(name) = password_target(left) else {
return None;
@ -63,29 +66,29 @@ pub(crate) fn compare_to_hardcoded_password_string(
},
comp.range(),
))
})
.collect()
}));
}
/// S105
pub(crate) fn assign_hardcoded_password_string(
checker: &mut Checker,
value: &Expr,
targets: &[Expr],
) -> Option<Diagnostic> {
) {
if string_literal(value)
.filter(|string| !string.is_empty())
.is_some()
{
for target in targets {
if let Some(name) = password_target(target) {
return Some(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
HardcodedPasswordString {
name: name.to_string(),
},
value.range(),
));
return;
}
}
}
None
}

View File

@ -67,7 +67,7 @@ fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Optio
return None;
};
// Only evaluate the full BinOp, not the nested components.
let Expr::BinOp(_ )= parent else {
let Expr::BinOp(_) = parent else {
if any_over_expr(expr, &has_string_literal) {
return Some(checker.generator().expr(expr));
}

View File

@ -21,21 +21,6 @@ impl Violation for RequestWithNoCertValidation {
}
}
const REQUESTS_HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"];
const HTTPX_METHODS: [&str; 11] = [
"get",
"options",
"head",
"post",
"put",
"patch",
"delete",
"request",
"stream",
"Client",
"AsyncClient",
];
/// S501
pub(crate) fn request_with_no_cert_validation(
checker: &mut Checker,
@ -46,16 +31,13 @@ pub(crate) fn request_with_no_cert_validation(
if let Some(target) = checker
.semantic()
.resolve_call_path(func)
.and_then(|call_path| {
if call_path.len() == 2 {
if call_path[0] == "requests" && REQUESTS_HTTP_VERBS.contains(&call_path[1]) {
return Some("requests");
.and_then(|call_path| match call_path.as_slice() {
["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete"] => {
Some("requests")
}
if call_path[0] == "httpx" && HTTPX_METHODS.contains(&call_path[1]) {
return Some("httpx");
}
}
None
["httpx", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request"
| "stream" | "Client" | "AsyncClient"] => Some("httpx"),
_ => None,
})
{
let call_args = SimpleCallArgs::new(args, keywords);

View File

@ -1,31 +1,28 @@
use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use ruff_python_ast::helpers::{is_const_none, SimpleCallArgs};
use crate::checkers::ast::Checker;
#[violation]
pub struct RequestWithoutTimeout {
pub timeout: Option<String>,
implicit: bool,
}
impl Violation for RequestWithoutTimeout {
#[derive_message_formats]
fn message(&self) -> String {
let RequestWithoutTimeout { timeout } = self;
match timeout {
Some(value) => {
format!("Probable use of requests call with timeout set to `{value}`")
}
None => format!("Probable use of requests call without timeout"),
let RequestWithoutTimeout { implicit } = self;
if *implicit {
format!("Probable use of requests call without timeout")
} else {
format!("Probable use of requests call with timeout set to `None`")
}
}
}
const HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"];
/// S113
pub(crate) fn request_without_timeout(
checker: &mut Checker,
@ -37,30 +34,26 @@ pub(crate) fn request_without_timeout(
.semantic()
.resolve_call_path(func)
.map_or(false, |call_path| {
HTTP_VERBS
.iter()
.any(|func_name| call_path.as_slice() == ["requests", func_name])
matches!(
call_path.as_slice(),
[
"requests",
"get" | "options" | "head" | "post" | "put" | "patch" | "delete"
]
)
})
{
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(timeout_arg) = call_args.keyword_argument("timeout") {
if let Some(timeout) = match timeout_arg {
Expr::Constant(ast::ExprConstant {
value: value @ Constant::None,
..
}) => Some(checker.generator().constant(value)),
_ => None,
} {
if let Some(timeout) = call_args.keyword_argument("timeout") {
if is_const_none(timeout) {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout {
timeout: Some(timeout),
},
timeout_arg.range(),
RequestWithoutTimeout { implicit: false },
timeout.range(),
));
}
} else {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { timeout: None },
RequestWithoutTimeout { implicit: true },
func.range(),
));
}

View File

@ -219,7 +219,7 @@ impl Violation for SuspiciousFTPLibUsage {
}
}
/// S001
/// S301, S302, S303, S304, S305, S306, S307, S308, S310, S311, S312, S313, S314, S315, S316, S317, S318, S319, S320, S321, S323
pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) {
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return;
@ -246,7 +246,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) {
// Mktemp
["tempfile", "mktemp"] => Some(SuspiciousMktempUsage.into()),
// Eval
["eval"] => Some(SuspiciousEvalUsage.into()),
["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()),
// MarkSafe
["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
// URLOpen

View File

@ -6,7 +6,7 @@ S102.py:3:5: S102 Use of `exec` detected
1 | def fn():
2 | # Error
3 | exec('x = 2')
| ^^^^^^^^^^^^^ S102
| ^^^^ S102
4 |
5 | exec('y = 3')
|
@ -16,7 +16,7 @@ S102.py:5:1: S102 Use of `exec` detected
3 | exec('x = 2')
4 |
5 | exec('y = 3')
| ^^^^^^^^^^^^^ S102
| ^^^^ S102
|

View File

@ -0,0 +1,20 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
---
S307.py:3:7: S307 Use of possibly insecure function; consider using `ast.literal_eval`
|
1 | import os
2 |
3 | print(eval("1+1")) # S307
| ^^^^^^^^^^^ S307
4 | print(eval("os.getcwd()")) # S307
|
S307.py:4:7: S307 Use of possibly insecure function; consider using `ast.literal_eval`
|
3 | print(eval("1+1")) # S307
4 | print(eval("os.getcwd()")) # S307
| ^^^^^^^^^^^^^^^^^^^ S307
|

View File

@ -161,19 +161,19 @@ pub(crate) fn abstract_base_class(
continue;
}
let (
Stmt::FunctionDef(ast::StmtFunctionDef {
decorator_list,
body,
name: method_name,
..
}) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
let (Stmt::FunctionDef(ast::StmtFunctionDef {
decorator_list,
body,
name: method_name,
..
})
) = stmt else {
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
decorator_list,
body,
name: method_name,
..
})) = stmt
else {
continue;
};

View File

@ -1,22 +1,20 @@
use rustpython_parser::ast::{self, Expr, Ranged, Stmt, WithItem};
use std::fmt;
use rustpython_parser::ast::{self, Expr, Ranged, WithItem};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum AssertionKind {
AssertRaises,
PytestRaises,
}
/// ## What it does
/// Checks for `self.assertRaises(Exception)` or `pytest.raises(Exception)`.
/// Checks for `assertRaises` and `pytest.raises` context managers that catch
/// `Exception` or `BaseException`.
///
/// ## Why is this bad?
/// These forms catch every `Exception`, which can lead to tests passing even
/// if, e.g., the code being tested is never executed due to a typo.
/// if, e.g., the code under consideration raises a `SyntaxError` or
/// `IndentationError`.
///
/// Either assert for a more specific exception (builtin or custom), or use
/// `assertRaisesRegex` or `pytest.raises(..., match=<REGEX>)` respectively.
@ -32,30 +30,61 @@ pub(crate) enum AssertionKind {
/// ```
#[violation]
pub struct AssertRaisesException {
kind: AssertionKind,
assertion: AssertionKind,
exception: ExceptionKind,
}
impl Violation for AssertRaisesException {
#[derive_message_formats]
fn message(&self) -> String {
match self.kind {
AssertionKind::AssertRaises => {
format!("`assertRaises(Exception)` should be considered evil")
let AssertRaisesException {
assertion,
exception,
} = self;
format!("`{assertion}({exception})` should be considered evil")
}
AssertionKind::PytestRaises => {
format!("`pytest.raises(Exception)` should be considered evil")
}
#[derive(Debug, PartialEq, Eq)]
enum AssertionKind {
AssertRaises,
PytestRaises,
}
impl fmt::Display for AssertionKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
AssertionKind::AssertRaises => fmt.write_str("assertRaises"),
AssertionKind::PytestRaises => fmt.write_str("pytest.raises"),
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum ExceptionKind {
BaseException,
Exception,
}
impl fmt::Display for ExceptionKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
ExceptionKind::BaseException => fmt.write_str("BaseException"),
ExceptionKind::Exception => fmt.write_str("Exception"),
}
}
}
/// B017
pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[WithItem]) {
let Some(item) = items.first() else {
return;
};
let item_context = &item.context_expr;
let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &item_context else {
pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) {
for item in items {
let Expr::Call(ast::ExprCall {
func,
args,
keywords,
range: _,
}) = &item.context_expr
else {
return;
};
if args.len() != 1 {
@ -65,18 +94,19 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items:
return;
}
if !checker
let Some(exception) = checker
.semantic()
.resolve_call_path(args.first().unwrap())
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["", "Exception"])
.and_then(|call_path| match call_path.as_slice() {
["", "Exception"] => Some(ExceptionKind::Exception),
["", "BaseException"] => Some(ExceptionKind::BaseException),
_ => None,
})
{
else {
return;
}
};
let kind = {
if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
let assertion = if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
{
AssertionKind::AssertRaises
} else if checker
@ -92,11 +122,14 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items:
AssertionKind::PytestRaises
} else {
return;
}
};
checker.diagnostics.push(Diagnostic::new(
AssertRaisesException { kind },
stmt.range(),
AssertRaisesException {
assertion,
exception,
},
item.range(),
));
}
}

View File

@ -59,7 +59,7 @@ pub(crate) fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr])
if attr != "environ" {
return;
}
let Expr::Name(ast::ExprName { id, .. } )= value.as_ref() else {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return;
};
if id != "os" {

View File

@ -166,7 +166,11 @@ pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHand
let mut seen: FxHashSet<CallPath> = FxHashSet::default();
let mut duplicates: FxHashMap<CallPath, Vec<&Expr>> = FxHashMap::default();
for handler in handlers {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_: Some(type_), .. }) = handler else {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_: Some(type_),
..
}) = handler
else {
continue;
};
match type_.as_ref() {

View File

@ -47,7 +47,7 @@ impl Violation for ExceptWithNonExceptionClasses {
/// This should leave any unstarred iterables alone (subsequently raising a
/// warning for B029).
fn flatten_starred_iterables(expr: &Expr) -> Vec<&Expr> {
let Expr::Tuple(ast::ExprTuple { elts, .. } )= expr else {
let Expr::Tuple(ast::ExprTuple { elts, .. }) = expr else {
return vec![expr];
};
let mut flattened_exprs: Vec<&Expr> = Vec::with_capacity(elts.len());

View File

@ -1,9 +1,8 @@
use rustc_hash::FxHashSet;
use rustpython_parser::ast::{self, Comprehension, Expr, ExprContext, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::collect_arg_names;
use ruff_python_ast::helpers::includes_arg_name;
use ruff_python_ast::types::Node;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
@ -58,19 +57,17 @@ impl Violation for FunctionUsesLoopVariable {
#[derive(Default)]
struct LoadedNamesVisitor<'a> {
// Tuple of: name, defining expression, and defining range.
loaded: Vec<(&'a str, &'a Expr)>,
// Tuple of: name, defining expression, and defining range.
stored: Vec<(&'a str, &'a Expr)>,
loaded: Vec<&'a ast::ExprName>,
stored: Vec<&'a ast::ExprName>,
}
/// `Visitor` to collect all used identifiers in a statement.
impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
match expr {
Expr::Name(ast::ExprName { id, ctx, range: _ }) => match ctx {
ExprContext::Load => self.loaded.push((id, expr)),
ExprContext::Store => self.stored.push((id, expr)),
Expr::Name(name) => match &name.ctx {
ExprContext::Load => self.loaded.push(name),
ExprContext::Store => self.stored.push(name),
ExprContext::Del => {}
},
_ => visitor::walk_expr(self, expr),
@ -80,7 +77,7 @@ impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> {
#[derive(Default)]
struct SuspiciousVariablesVisitor<'a> {
names: Vec<(&'a str, &'a Expr)>,
names: Vec<&'a ast::ExprName>,
safe_functions: Vec<&'a Expr>,
}
@ -95,17 +92,20 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
let mut visitor = LoadedNamesVisitor::default();
visitor.visit_body(body);
// Collect all argument names.
let mut arg_names = collect_arg_names(args);
arg_names.extend(visitor.stored.iter().map(|(id, ..)| id));
// Treat any non-arguments as "suspicious".
self.names.extend(
visitor
.loaded
.into_iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
self.names
.extend(visitor.loaded.into_iter().filter(|loaded| {
if visitor.stored.iter().any(|stored| stored.id == loaded.id) {
return false;
}
if includes_arg_name(&loaded.id, args) {
return false;
}
true
}));
return;
}
Stmt::Return(ast::StmtReturn {
@ -132,10 +132,9 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
}) => {
match func.as_ref() {
Expr::Name(ast::ExprName { id, .. }) => {
let id = id.as_str();
if id == "filter" || id == "reduce" || id == "map" {
if matches!(id.as_str(), "filter" | "reduce" | "map") {
for arg in args {
if matches!(arg, Expr::Lambda(_)) {
if arg.is_lambda_expr() {
self.safe_functions.push(arg);
}
}
@ -159,7 +158,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
for keyword in keywords {
if keyword.arg.as_ref().map_or(false, |arg| arg == "key")
&& matches!(keyword.value, Expr::Lambda(_))
&& keyword.value.is_lambda_expr()
{
self.safe_functions.push(&keyword.value);
}
@ -175,17 +174,19 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
let mut visitor = LoadedNamesVisitor::default();
visitor.visit_expr(body);
// Collect all argument names.
let mut arg_names = collect_arg_names(args);
arg_names.extend(visitor.stored.iter().map(|(id, ..)| id));
// Treat any non-arguments as "suspicious".
self.names.extend(
visitor
.loaded
.iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
self.names
.extend(visitor.loaded.into_iter().filter(|loaded| {
if visitor.stored.iter().any(|stored| stored.id == loaded.id) {
return false;
}
if includes_arg_name(&loaded.id, args) {
return false;
}
true
}));
return;
}
@ -198,7 +199,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
#[derive(Default)]
struct NamesFromAssignmentsVisitor<'a> {
names: FxHashSet<&'a str>,
names: Vec<&'a str>,
}
/// `Visitor` to collect all names used in an assignment expression.
@ -206,7 +207,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
match expr {
Expr::Name(ast::ExprName { id, .. }) => {
self.names.insert(id.as_str());
self.names.push(id.as_str());
}
Expr::Starred(ast::ExprStarred { value, .. }) => {
self.visit_expr(value);
@ -223,7 +224,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> {
#[derive(Default)]
struct AssignedNamesVisitor<'a> {
names: FxHashSet<&'a str>,
names: Vec<&'a str>,
}
/// `Visitor` to collect all used identifiers in a statement.
@ -257,7 +258,7 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
}
fn visit_expr(&mut self, expr: &'a Expr) {
if matches!(expr, Expr::Lambda(_)) {
if expr.is_lambda_expr() {
// Don't recurse.
return;
}
@ -300,15 +301,15 @@ pub(crate) fn function_uses_loop_variable<'a>(checker: &mut Checker<'a>, node: &
// If a variable was used in a function or lambda body, and assigned in the
// loop, flag it.
for (name, expr) in suspicious_variables {
if reassigned_in_loop.contains(name) {
if !checker.flake8_bugbear_seen.contains(&expr) {
checker.flake8_bugbear_seen.push(expr);
for name in suspicious_variables {
if reassigned_in_loop.contains(&name.id.as_str()) {
if !checker.flake8_bugbear_seen.contains(&name) {
checker.flake8_bugbear_seen.push(name);
checker.diagnostics.push(Diagnostic::new(
FunctionUsesLoopVariable {
name: name.to_string(),
name: name.id.to_string(),
},
expr.range(),
name.range(),
));
}
}

View File

@ -64,7 +64,7 @@ pub(crate) fn getattr_with_constant(
func: &Expr,
args: &[Expr],
) {
let Expr::Name(ast::ExprName { id, .. } )= func else {
let Expr::Name(ast::ExprName { id, .. }) = func else {
return;
};
if id != "getattr" {
@ -76,7 +76,8 @@ pub(crate) fn getattr_with_constant(
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
} )= arg else {
}) = arg
else {
return;
};
if !is_identifier(value) {

View File

@ -69,7 +69,7 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Argume
.chain(&arguments.args)
.chain(&arguments.kwonlyargs)
{
let Some(default)= default else {
let Some(default) = default else {
continue;
};

View File

@ -59,7 +59,11 @@ pub(crate) fn redundant_tuple_in_exception_handler(
handlers: &[ExceptHandler],
) {
for handler in handlers {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_: Some(type_), .. }) = handler else {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_: Some(type_),
..
}) = handler
else {
continue;
};
let Expr::Tuple(ast::ExprTuple { elts, .. }) = type_.as_ref() else {

View File

@ -82,7 +82,8 @@ pub(crate) fn setattr_with_constant(
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(name),
..
} )= name else {
}) = name
else {
return;
};
if !is_identifier(name) {

View File

@ -64,7 +64,7 @@ pub(crate) fn star_arg_unpacking_after_keyword_arg(
return;
};
for arg in args {
let Expr::Starred (_) = arg else {
let Expr::Starred(_) = arg else {
continue;
};
if arg.start() <= keyword.start() {

View File

@ -62,7 +62,8 @@ pub(crate) fn strip_with_multi_characters(
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
..
} )= &args[0] else {
}) = &args[0]
else {
return;
};

View File

@ -45,7 +45,7 @@ pub(crate) fn unary_prefix_increment(
if !matches!(op, UnaryOp::UAdd) {
return;
}
let Expr::UnaryOp(ast::ExprUnaryOp { op, .. })= operand else {
let Expr::UnaryOp(ast::ExprUnaryOp { op, .. }) = operand else {
return;
};
if !matches!(op, UnaryOp::UAdd) {

View File

@ -63,8 +63,8 @@ pub(crate) fn unreliable_callable_check(
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(s),
..
}) = &args[1] else
{
}) = &args[1]
else {
return;
};
if s != "__call__" {

View File

@ -68,7 +68,13 @@ pub(crate) fn zip_without_explicit_strict(
/// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to
/// `itertools.cycle` or similar).
fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Call(ast::ExprCall { func, args, keywords, .. }) = &arg else {
let Expr::Call(ast::ExprCall {
func,
args,
keywords,
..
}) = &arg
else {
return false;
};

View File

@ -1,27 +1,38 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B017.py:23:9: B017 `assertRaises(Exception)` should be considered evil
B017.py:23:14: B017 `assertRaises(Exception)` should be considered evil
|
21 | class Foobar(unittest.TestCase):
22 | def evil_raises(self) -> None:
23 | with self.assertRaises(Exception):
| _________^
24 | | raise Exception("Evil I say!")
| |__________________________________________^ B017
25 |
26 | def context_manager_raises(self) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
24 | raise Exception("Evil I say!")
|
B017.py:41:5: B017 `pytest.raises(Exception)` should be considered evil
B017.py:27:14: B017 `assertRaises(BaseException)` should be considered evil
|
40 | def test_pytest_raises():
41 | with pytest.raises(Exception):
| _____^
42 | | raise ValueError("Hello")
| |_________________________________^ B017
43 |
44 | with pytest.raises(Exception, "hello"):
26 | def also_evil_raises(self) -> None:
27 | with self.assertRaises(BaseException):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
28 | raise Exception("Evil I say!")
|
B017.py:45:10: B017 `pytest.raises(Exception)` should be considered evil
|
44 | def test_pytest_raises():
45 | with pytest.raises(Exception):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
46 | raise ValueError("Hello")
|
B017.py:48:10: B017 `pytest.raises(Exception)` should be considered evil
|
46 | raise ValueError("Hello")
47 |
48 | with pytest.raises(Exception), pytest.raises(ValueError):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
49 | raise ValueError("Hello")
|

View File

@ -109,7 +109,8 @@ pub(crate) fn fix_unnecessary_generator_dict(
// Extract the (k, v) from `(k, v) for ...`.
let generator_exp = match_generator_exp(&arg.value)?;
let tuple = match_tuple(&generator_exp.elt)?;
let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] else {
let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..]
else {
bail!("Expected tuple to contain two elements");
};
@ -188,9 +189,10 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict(
let tuple = match_tuple(&list_comp.elt)?;
let [Element::Simple {
value: key, ..
}, Element::Simple { value, .. }] = &tuple.elements[..] else { bail!("Expected tuple with two elements"); };
let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..]
else {
bail!("Expected tuple with two elements");
};
tree = Expression::DictComp(Box::new(DictComp {
key: Box::new(key.clone()),
@ -982,14 +984,10 @@ pub(crate) fn fix_unnecessary_map(
}
let Some(Element::Simple { value: key, .. }) = &tuple.elements.get(0) else {
bail!(
"Expected tuple to contain a key as the first element"
);
bail!("Expected tuple to contain a key as the first element");
};
let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) else {
bail!(
"Expected tuple to contain a key as the second element"
);
bail!("Expected tuple to contain a key as the second element");
};
(key, value)
@ -1063,9 +1061,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
let call = match_call_mut(&mut tree)?;
let Expression::ListComp(list_comp) = &call.args[0].value else {
bail!(
"Expected Expression::ListComp"
);
bail!("Expected Expression::ListComp");
};
let mut new_empty_lines = vec![];

View File

@ -66,11 +66,13 @@ pub(crate) fn unnecessary_comprehension_any_all(
if !keywords.is_empty() {
return;
}
let Expr::Name(ast::ExprName { id, .. } )= func else {
let Expr::Name(ast::ExprName { id, .. }) = func else {
return;
};
if (matches!(id.as_str(), "all" | "any")) && args.len() == 1 {
let (Expr::ListComp(ast::ExprListComp { elt, .. } )| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0] else {
let (Expr::ListComp(ast::ExprListComp { elt, .. })
| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0]
else {
return;
};
if contains_await(elt) {

View File

@ -84,7 +84,7 @@ pub(crate) fn unnecessary_double_cast_or_process(
let Some(arg) = args.first() else {
return;
};
let Expr::Call(ast::ExprCall { func, ..} )= arg else {
let Expr::Call(ast::ExprCall { func, .. }) = arg else {
return;
};
let Some(inner) = helpers::expr_name(func) else {

View File

@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_dict(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords)
else {
return;
};
if let Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) = argument {

View File

@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_list(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("list", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("list", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("list") {

View File

@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_set(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("set") {

View File

@ -47,7 +47,9 @@ pub(crate) fn unnecessary_list_comprehension_dict(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("dict") {

View File

@ -47,7 +47,9 @@ pub(crate) fn unnecessary_list_comprehension_set(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("set") {

View File

@ -54,7 +54,9 @@ pub(crate) fn unnecessary_literal_dict(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("dict") {

View File

@ -55,7 +55,9 @@ pub(crate) fn unnecessary_literal_set(
args: &[Expr],
keywords: &[Keyword],
) {
let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else {
let Some(argument) =
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
else {
return;
};
if !checker.semantic().is_builtin("set") {

View File

@ -127,7 +127,9 @@ pub(crate) fn unnecessary_map(
if args.len() != 2 {
return;
}
let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else {
let Some(argument) =
helpers::first_argument_with_matching_function("map", func, args)
else {
return;
};
if let Expr::Lambda(_) = argument {
@ -155,7 +157,9 @@ pub(crate) fn unnecessary_map(
if args.len() == 1 {
if let Expr::Call(ast::ExprCall { func, args, .. }) = &args[0] {
let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else {
let Some(argument) =
helpers::first_argument_with_matching_function("map", func, args)
else {
return;
};
if let Expr::Lambda(ast::ExprLambda { body, .. }) = argument {

View File

@ -64,7 +64,13 @@ pub(crate) fn unnecessary_subscript_reversal(
let Expr::Subscript(ast::ExprSubscript { slice, .. }) = first_arg else {
return;
};
let Expr::Slice(ast::ExprSlice { lower, upper, step, range: _ }) = slice.as_ref() else {
let Expr::Slice(ast::ExprSlice {
lower,
upper,
step,
range: _,
}) = slice.as_ref()
else {
return;
};
if lower.is_some() || upper.is_some() {
@ -77,13 +83,15 @@ pub(crate) fn unnecessary_subscript_reversal(
op: UnaryOp::USub,
operand,
range: _,
}) = step.as_ref() else {
}) = step.as_ref()
else {
return;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Int(val),
..
}) = operand.as_ref() else {
}) = operand.as_ref()
else {
return;
};
if *val != BigInt::from(1) {

View File

@ -49,11 +49,13 @@ pub(crate) fn call_datetime_strptime_without_zone(
}
};
let (Some(grandparent), Some(parent)) = (checker.semantic().expr_grandparent(), checker.semantic().expr_parent()) else {
checker.diagnostics.push(Diagnostic::new(
CallDatetimeStrptimeWithoutZone,
location,
));
let (Some(grandparent), Some(parent)) = (
checker.semantic().expr_grandparent(),
checker.semantic().expr_parent(),
) else {
checker
.diagnostics
.push(Diagnostic::new(CallDatetimeStrptimeWithoutZone, location));
return;
};

View File

@ -85,7 +85,7 @@ pub(crate) fn locals_in_render_function(
fn is_locals_call(expr: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return false
return false;
};
semantic.resolve_call_path(func).map_or(false, |call_path| {
matches!(call_path.as_slice(), ["", "locals"])

View File

@ -52,21 +52,20 @@ impl Violation for DjangoModelWithoutDunderStr {
/// DJ008
pub(crate) fn model_without_dunder_str(
checker: &Checker,
bases: &[Expr],
body: &[Stmt],
class_location: &Stmt,
) -> Option<Diagnostic> {
checker: &mut Checker,
ast::StmtClassDef {
name, bases, body, ..
}: &ast::StmtClassDef,
) {
if !is_non_abstract_model(bases, body, checker.semantic()) {
return None;
return;
}
if !has_dunder_method(body) {
return Some(Diagnostic::new(
DjangoModelWithoutDunderStr,
class_location.range(),
));
if has_dunder_method(body) {
return;
}
None
checker
.diagnostics
.push(Diagnostic::new(DjangoModelWithoutDunderStr, name.range()));
}
fn has_dunder_method(body: &[Stmt]) -> bool {
@ -96,18 +95,18 @@ fn is_non_abstract_model(bases: &[Expr], body: &[Stmt], semantic: &SemanticModel
/// Check if class is abstract, in terms of Django model inheritance.
fn is_model_abstract(body: &[Stmt]) -> bool {
for element in body.iter() {
let Stmt::ClassDef(ast::StmtClassDef {name, body, ..}) = element else {
continue
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
continue;
};
if name != "Meta" {
continue;
}
for element in body.iter() {
let Stmt::Assign(ast::StmtAssign {targets, value, ..}) = element else {
let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = element else {
continue;
};
for target in targets.iter() {
let Expr::Name(ast::ExprName {id , ..}) = target else {
let Expr::Name(ast::ExprName { id, .. }) = target else {
continue;
};
if id != "abstract" {

View File

@ -1,8 +1,9 @@
use rustpython_parser::ast::{self, Decorator, Expr, Ranged};
use rustpython_parser::ast::{Decorator, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that Django's `@receiver` decorator is listed first, prior to
@ -48,25 +49,19 @@ impl Violation for DjangoNonLeadingReceiverDecorator {
}
/// DJ013
pub(crate) fn non_leading_receiver_decorator<'a, F>(
decorator_list: &'a [Decorator],
resolve_call_path: F,
) -> Vec<Diagnostic>
where
F: Fn(&'a Expr) -> Option<CallPath<'a>>,
{
let mut diagnostics = vec![];
pub(crate) fn non_leading_receiver_decorator(checker: &mut Checker, decorator_list: &[Decorator]) {
let mut seen_receiver = false;
for (i, decorator) in decorator_list.iter().enumerate() {
let is_receiver = match &decorator.expression {
Expr::Call(ast::ExprCall { func, .. }) => resolve_call_path(func)
let is_receiver = decorator.expression.as_call_expr().map_or(false, |call| {
checker
.semantic()
.resolve_call_path(&call.func)
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["django", "dispatch", "receiver"])
}),
_ => false,
};
})
});
if i > 0 && is_receiver && !seen_receiver {
diagnostics.push(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
DjangoNonLeadingReceiverDecorator,
decorator.range(),
));
@ -77,5 +72,4 @@ where
seen_receiver = true;
}
}
diagnostics
}

View File

@ -51,24 +51,14 @@ impl Violation for DjangoNullableModelStringField {
}
}
const NOT_NULL_TRUE_FIELDS: [&str; 6] = [
"CharField",
"TextField",
"SlugField",
"EmailField",
"FilePathField",
"URLField",
];
/// DJ001
pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> Vec<Diagnostic> {
let mut errors = Vec::new();
pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt]) {
for statement in body.iter() {
let Stmt::Assign(ast::StmtAssign {value, ..}) = statement else {
continue
let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else {
continue;
};
if let Some(field_name) = is_nullable_field(checker, value) {
errors.push(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
DjangoNullableModelStringField {
field_name: field_name.to_string(),
},
@ -76,11 +66,10 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> V
));
}
}
errors
}
fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a str> {
let Expr::Call(ast::ExprCall {func, keywords, ..}) = value else {
let Expr::Call(ast::ExprCall { func, keywords, .. }) = value else {
return None;
};
@ -88,7 +77,10 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st
return None;
};
if !NOT_NULL_TRUE_FIELDS.contains(&valid_field_name) {
if !matches!(
valid_field_name,
"CharField" | "TextField" | "SlugField" | "EmailField" | "FilePathField" | "URLField"
) {
return None;
}
@ -97,7 +89,7 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st
let mut unique_key = false;
for keyword in keywords.iter() {
let Some(argument) = &keyword.arg else {
continue
continue;
};
if !is_const_true(&keyword.value) {
continue;

View File

@ -63,20 +63,23 @@ use super::helpers;
/// [Django Style Guide]: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#model-style
#[violation]
pub struct DjangoUnorderedBodyContentInModel {
elem_type: ContentType,
before: ContentType,
element_type: ContentType,
prev_element_type: ContentType,
}
impl Violation for DjangoUnorderedBodyContentInModel {
#[derive_message_formats]
fn message(&self) -> String {
let DjangoUnorderedBodyContentInModel { elem_type, before } = self;
format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {elem_type} should come before {before}")
let DjangoUnorderedBodyContentInModel {
element_type,
prev_element_type,
} = self;
format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {element_type} should come before {prev_element_type}")
}
}
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub(crate) enum ContentType {
enum ContentType {
FieldDeclaration,
ManagerDeclaration,
MetaClass,
@ -149,24 +152,38 @@ pub(crate) fn unordered_body_content_in_model(
{
return;
}
let mut elements_type_found = Vec::new();
// Track all the element types we've seen so far.
let mut element_types = Vec::new();
let mut prev_element_type = None;
for element in body.iter() {
let Some(current_element_type) = get_element_type(element, checker.semantic()) else {
let Some(element_type) = get_element_type(element, checker.semantic()) else {
continue;
};
let Some(&element_type) = elements_type_found
// Skip consecutive elements of the same type. It's less noisy to only report
// violations at type boundaries (e.g., avoid raising a violation for _every_
// field declaration that's out of order).
if prev_element_type == Some(element_type) {
continue;
}
prev_element_type = Some(element_type);
if let Some(&prev_element_type) = element_types
.iter()
.find(|&&element_type| element_type > current_element_type) else {
elements_type_found.push(current_element_type);
continue;
};
.find(|&&prev_element_type| prev_element_type > element_type)
{
let diagnostic = Diagnostic::new(
DjangoUnorderedBodyContentInModel {
elem_type: current_element_type,
before: element_type,
element_type,
prev_element_type,
},
element.range(),
);
checker.diagnostics.push(diagnostic);
} else {
element_types.push(element_type);
}
}
}

View File

@ -1,58 +1,26 @@
---
source: crates/ruff/src/rules/flake8_django/mod.rs
---
DJ008.py:6:1: DJ008 Model does not define `__str__` method
DJ008.py:6:7: DJ008 Model does not define `__str__` method
|
5 | # Models without __str__
6 | / class TestModel1(models.Model):
7 | | new_field = models.CharField(max_length=10)
8 | |
9 | | class Meta:
10 | | verbose_name = "test model"
11 | | verbose_name_plural = "test models"
12 | |
13 | | @property
14 | | def my_brand_new_property(self):
15 | | return 1
16 | |
17 | | def my_beautiful_method(self):
18 | | return 2
| |________________^ DJ008
5 | # Models without __str__
6 | class TestModel1(models.Model):
| ^^^^^^^^^^ DJ008
7 | new_field = models.CharField(max_length=10)
|
DJ008.py:21:1: DJ008 Model does not define `__str__` method
DJ008.py:21:7: DJ008 Model does not define `__str__` method
|
21 | / class TestModel2(Model):
22 | | new_field = models.CharField(max_length=10)
23 | |
24 | | class Meta:
25 | | verbose_name = "test model"
26 | | verbose_name_plural = "test models"
27 | |
28 | | @property
29 | | def my_brand_new_property(self):
30 | | return 1
31 | |
32 | | def my_beautiful_method(self):
33 | | return 2
| |________________^ DJ008
21 | class TestModel2(Model):
| ^^^^^^^^^^ DJ008
22 | new_field = models.CharField(max_length=10)
|
DJ008.py:36:1: DJ008 Model does not define `__str__` method
DJ008.py:36:7: DJ008 Model does not define `__str__` method
|
36 | / class TestModel3(Model):
37 | | new_field = models.CharField(max_length=10)
38 | |
39 | | class Meta:
40 | | abstract = False
41 | |
42 | | @property
43 | | def my_brand_new_property(self):
44 | | return 1
45 | |
46 | | def my_beautiful_method(self):
47 | | return 2
| |________________^ DJ008
36 | class TestModel3(Model):
| ^^^^^^^^^^ DJ008
37 | new_field = models.CharField(max_length=10)
|

View File

@ -37,4 +37,21 @@ DJ012.py:69:5: DJ012 Order of model's inner classes, methods, and fields does no
| |____________^ DJ012
|
DJ012.py:123:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class
|
121 | verbose_name = "test"
122 |
123 | first_name = models.CharField(max_length=32)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012
124 | last_name = models.CharField(max_length=32)
|
DJ012.py:129:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class
|
127 | pass
128 |
129 | middle_name = models.CharField(max_length=32)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012
|

View File

@ -3,6 +3,8 @@ use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[violation]
pub struct FStringInGetTextFuncCall;
@ -14,11 +16,12 @@ impl Violation for FStringInGetTextFuncCall {
}
/// INT001
pub(crate) fn f_string_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
pub(crate) fn f_string_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) {
if let Some(first) = args.first() {
if first.is_joined_str_expr() {
return Some(Diagnostic::new(FStringInGetTextFuncCall {}, first.range()));
checker
.diagnostics
.push(Diagnostic::new(FStringInGetTextFuncCall {}, first.range()));
}
}
None
}

View File

@ -3,6 +3,8 @@ use rustpython_parser::ast::{self, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[violation]
pub struct FormatInGetTextFuncCall;
@ -14,15 +16,16 @@ impl Violation for FormatInGetTextFuncCall {
}
/// INT002
pub(crate) fn format_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
pub(crate) fn format_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) {
if let Some(first) = args.first() {
if let Expr::Call(ast::ExprCall { func, .. }) = &first {
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() {
if attr == "format" {
return Some(Diagnostic::new(FormatInGetTextFuncCall {}, first.range()));
checker
.diagnostics
.push(Diagnostic::new(FormatInGetTextFuncCall {}, first.range()));
}
}
}
}
None
}

View File

@ -1,5 +1,6 @@
use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@ -14,7 +15,7 @@ impl Violation for PrintfInGetTextFuncCall {
}
/// INT003
pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
pub(crate) fn printf_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) {
if let Some(first) = args.first() {
if let Expr::BinOp(ast::ExprBinOp {
op: Operator::Mod { .. },
@ -27,9 +28,10 @@ pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
..
}) = left.as_ref()
{
return Some(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range()));
checker
.diagnostics
.push(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range()));
}
}
}
None
}

View File

@ -60,7 +60,12 @@ impl AlwaysAutofixableViolation for MultipleStartsEndsWith {
/// PIE810
pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
let Expr::BoolOp(ast::ExprBoolOp { op: BoolOp::Or, values, range: _ }) = expr else {
let Expr::BoolOp(ast::ExprBoolOp {
op: BoolOp::Or,
values,
range: _,
}) = expr
else {
return;
};
@ -70,24 +75,25 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
func,
args,
keywords,
range: _
}) = &call else {
continue
range: _,
}) = &call
else {
continue;
};
if !(args.len() == 1 && keywords.is_empty()) {
continue;
}
let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func.as_ref() else {
continue
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else {
continue;
};
if attr != "startswith" && attr != "endswith" {
continue;
}
let Expr::Name(ast::ExprName { id: arg_name, .. } )= value.as_ref() else {
continue
let Expr::Name(ast::ExprName { id: arg_name, .. }) = value.as_ref() else {
continue;
};
duplicates
@ -110,8 +116,17 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
.iter()
.map(|index| &values[*index])
.map(|expr| {
let Expr::Call(ast::ExprCall { func: _, args, keywords: _, range: _}) = expr else {
unreachable!("{}", format!("Indices should only contain `{attr_name}` calls"))
let Expr::Call(ast::ExprCall {
func: _,
args,
keywords: _,
range: _,
}) = expr
else {
unreachable!(
"{}",
format!("Indices should only contain `{attr_name}` calls")
)
};
args.get(0)
.unwrap_or_else(|| panic!("`{attr_name}` should have one argument"))

View File

@ -22,6 +22,8 @@ mod tests {
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))]
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))]
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))]
#[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.py"))]
#[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.pyi"))]
#[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))]
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))]
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))]
@ -56,6 +58,8 @@ mod tests {
#[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.pyi"))]
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.py"))]
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))]
#[test_case(Rule::PatchVersionComparison, Path::new("PYI004.py"))]
#[test_case(Rule::PatchVersionComparison, Path::new("PYI004.pyi"))]
#[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))]
#[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))]
#[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))]
@ -72,6 +76,10 @@ mod tests {
#[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.pyi"))]
#[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.py"))]
#[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.pyi"))]
#[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.py"))]
#[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.pyi"))]
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.py"))]
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.pyi"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@ -1,4 +1,4 @@
use rustpython_parser::ast::{CmpOp, Expr, Ranged};
use rustpython_parser::ast::{self, CmpOp, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@ -52,19 +52,23 @@ pub struct BadVersionInfoComparison;
impl Violation for BadVersionInfoComparison {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `<` or `>=` for version info comparisons")
format!("Use `<` or `>=` for `sys.version_info` comparisons")
}
}
/// PYI006
pub(crate) fn bad_version_info_comparison(
checker: &mut Checker,
expr: &Expr,
left: &Expr,
ops: &[CmpOp],
comparators: &[Expr],
) {
let ([op], [_right]) = (ops, comparators) else {
pub(crate) fn bad_version_info_comparison(checker: &mut Checker, test: &Expr) {
let Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
..
}) = test
else {
return;
};
let ([op], [_right]) = (ops.as_slice(), comparators.as_slice()) else {
return;
};
@ -78,8 +82,11 @@ pub(crate) fn bad_version_info_comparison(
return;
}
if !matches!(op, CmpOp::Lt | CmpOp::GtE) {
let diagnostic = Diagnostic::new(BadVersionInfoComparison, expr.range());
checker.diagnostics.push(diagnostic);
if matches!(op, CmpOp::Lt | CmpOp::GtE) {
return;
}
checker
.diagnostics
.push(Diagnostic::new(BadVersionInfoComparison, test.range()));
}

View File

@ -0,0 +1,80 @@
use rustpython_parser::ast::{self, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `if` statements with complex conditionals in stubs.
///
/// ## Why is this bad?
/// Stub files support simple conditionals to test for differences in Python
/// versions and platforms. However, type checkers only understand a limited
/// subset of these conditionals; complex conditionals may result in false
/// positives or false negatives.
///
/// ## Example
/// ```python
/// import sys
///
/// if (2, 7) < sys.version_info < (3, 5):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// if sys.version_info < (3, 5):
/// ...
/// ```
#[violation]
pub struct ComplexIfStatementInStub;
impl Violation for ComplexIfStatementInStub {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"`if`` test must be a simple comparison against `sys.platform` or `sys.version_info`"
)
}
}
/// PYI002
pub(crate) fn complex_if_statement_in_stub(checker: &mut Checker, test: &Expr) {
let Expr::Compare(ast::ExprCompare {
left, comparators, ..
}) = test
else {
checker
.diagnostics
.push(Diagnostic::new(ComplexIfStatementInStub, test.range()));
return;
};
if comparators.len() != 1 {
checker
.diagnostics
.push(Diagnostic::new(ComplexIfStatementInStub, test.range()));
return;
}
if left.is_subscript_expr() {
return;
}
if checker
.semantic()
.resolve_call_path(left)
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["sys", "version_info" | "platform"])
})
{
return;
}
checker
.diagnostics
.push(Diagnostic::new(ComplexIfStatementInStub, test.range()));
}

View File

@ -70,15 +70,12 @@ pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &De
kind: MemberKind::Method,
stmt,
..
}) = definition else {
}) = definition
else {
return;
};
let Stmt::FunctionDef(ast::StmtFunctionDef {
name,
returns,
..
}) = stmt else {
let Stmt::FunctionDef(ast::StmtFunctionDef { name, returns, .. }) = stmt else {
return;
};

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