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 crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf
ruff.schema.json linguist-generated=true text=auto eol=lf ruff.schema.json linguist-generated=true text=auto eol=lf
*.md.snap linguist-language=Markdown

View File

@ -1,5 +1,31 @@
# Breaking Changes # 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 ## 0.0.268
### The `keep-runtime-typing` setting has been removed ([#4427](https://github.com/astral-sh/ruff/pull/4427)) ### 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]] [[package]]
name = "flake8-to-ruff" name = "flake8-to-ruff"
version = "0.0.275" version = "0.0.276"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1829,7 +1829,7 @@ dependencies = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.0.275" version = "0.0.276"
dependencies = [ dependencies = [
"annotate-snippets 0.9.1", "annotate-snippets 0.9.1",
"anyhow", "anyhow",
@ -1865,6 +1865,7 @@ dependencies = [
"result-like", "result-like",
"ruff_cache", "ruff_cache",
"ruff_diagnostics", "ruff_diagnostics",
"ruff_index",
"ruff_macros", "ruff_macros",
"ruff_python_ast", "ruff_python_ast",
"ruff_python_semantic", "ruff_python_semantic",
@ -1926,7 +1927,7 @@ dependencies = [
[[package]] [[package]]
name = "ruff_cli" name = "ruff_cli"
version = "0.0.275" version = "0.0.276"
dependencies = [ dependencies = [
"annotate-snippets 0.9.1", "annotate-snippets 0.9.1",
"anyhow", "anyhow",

View File

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

View File

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

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ruff" name = "ruff"
version = "0.0.275" version = "0.0.276"
publish = false publish = false
authors = { workspace = true } authors = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
@ -17,6 +17,7 @@ name = "ruff"
[dependencies] [dependencies]
ruff_cache = { path = "../ruff_cache" } ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] } ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_index = { path = "../ruff_index" }
ruff_macros = { path = "../ruff_macros" } ruff_macros = { path = "../ruff_macros" }
ruff_python_whitespace = { path = "../ruff_python_whitespace" } ruff_python_whitespace = { path = "../ruff_python_whitespace" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] } ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
@ -88,3 +89,5 @@ colored = { workspace = true, features = ["no-color"] }
[features] [features]
default = [] default = []
schemars = ["dep:schemars"] 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): with self.assertRaises(Exception):
raise Exception("Evil I say!") 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: def context_manager_raises(self) -> None:
with self.assertRaises(Exception) as ex: with self.assertRaises(Exception) as ex:
raise Exception("Context manager is good") raise Exception("Context manager is good")
@ -41,6 +45,9 @@ def test_pytest_raises():
with pytest.raises(Exception): with pytest.raises(Exception):
raise ValueError("Hello") raise ValueError("Hello")
with pytest.raises(Exception), pytest.raises(ValueError):
raise ValueError("Hello")
with pytest.raises(Exception, "hello"): with pytest.raises(Exception, "hello"):
raise ValueError("This is fine") raise ValueError("This is fine")

View File

@ -111,3 +111,19 @@ class PerfectlyFine(models.Model):
@property @property
def random_property(self): def random_property(self):
return "%s" % 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 field28 = builtins.str
field29 = str field29 = str
field30 = str | bytes | None field30 = str | bytes | None
field31: typing.Final = field30

View File

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

View File

@ -1,5 +1,6 @@
# Do this (new version) # Do this (new version)
from numpy.random import default_rng from numpy.random import default_rng
rng = default_rng() rng = default_rng()
vals = rng.standard_normal(10) vals = rng.standard_normal(10)
more_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) # instead of this (legacy version)
from numpy import random from numpy import random
vals = random.standard_normal(10) vals = random.standard_normal(10)
more_vals = random.standard_normal(10) more_vals = random.standard_normal(10)
numbers = random.integers(high, size=5) numbers = random.integers(high, size=5)
import numpy import numpy
numpy.random.seed() numpy.random.seed()
numpy.random.get_state() numpy.random.get_state()
numpy.random.set_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.drop(["a"], axis=1, inplace=True) x.y.drop(["a"], axis=1, inplace=True)
x["y"].drop(["a"], axis=1, inplace=True)
x.drop( x.drop(
inplace=True, inplace=True,
@ -23,6 +25,7 @@ x.drop(["a"], axis=1, **kwargs, inplace=True)
x.drop(["a"], axis=1, inplace=True, **kwargs) x.drop(["a"], axis=1, inplace=True, **kwargs)
f(x.drop(["a"], axis=1, inplace=True)) 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 import torch
torch.m.ReLU(inplace=True) # safe because this isn't a pandas call 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" "abc"
"def" "def"
)).encode() )).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::noqa::NoqaMapping;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rules::flake8_builtins::helpers::AnyShadowing; use crate::rules::flake8_builtins::helpers::AnyShadowing;
use crate::rules::{ use crate::rules::{
airflow, flake8_2020, flake8_annotations, flake8_async, flake8_bandit, flake8_blind_except, airflow, flake8_2020, flake8_annotations, flake8_async, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez,
@ -71,7 +72,7 @@ pub(crate) struct Checker<'a> {
deferred: Deferred<'a>, deferred: Deferred<'a>,
pub(crate) diagnostics: Vec<Diagnostic>, pub(crate) diagnostics: Vec<Diagnostic>,
// Check-specific state. // Check-specific state.
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>, pub(crate) flake8_bugbear_seen: Vec<&'a ast::ExprName>,
} }
impl<'a> Checker<'a> { impl<'a> Checker<'a> {
@ -358,11 +359,7 @@ where
.. ..
}) => { }) => {
if self.enabled(Rule::DjangoNonLeadingReceiverDecorator) { if self.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
self.diagnostics flake8_django::rules::non_leading_receiver_decorator(self, decorator_list);
.extend(flake8_django::rules::non_leading_receiver_decorator(
decorator_list,
|expr| self.semantic.resolve_call_path(expr),
));
} }
if self.enabled(Rule::AmbiguousFunctionName) { if self.enabled(Rule::AmbiguousFunctionName) {
if let Some(diagnostic) = if let Some(diagnostic) =
@ -504,8 +501,7 @@ where
} }
} }
if self.enabled(Rule::HardcodedPasswordDefault) { if self.enabled(Rule::HardcodedPasswordDefault) {
self.diagnostics flake8_bandit::rules::hardcoded_password_default(self, args);
.extend(flake8_bandit::rules::hardcoded_password_default(args));
} }
if self.enabled(Rule::PropertyWithParameters) { if self.enabled(Rule::PropertyWithParameters) {
pylint::rules::property_with_parameters(self, stmt, decorator_list, args); 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(_) => { Stmt::Return(_) => {
if self.enabled(Rule::ReturnOutsideFunction) { if self.enabled(Rule::ReturnOutsideFunction) {
@ -643,10 +644,7 @@ where
}, },
) => { ) => {
if self.enabled(Rule::DjangoNullableModelStringField) { if self.enabled(Rule::DjangoNullableModelStringField) {
self.diagnostics flake8_django::rules::nullable_model_string_field(self, body);
.extend(flake8_django::rules::nullable_model_string_field(
self, body,
));
} }
if self.enabled(Rule::DjangoExcludeWithModelForm) { if self.enabled(Rule::DjangoExcludeWithModelForm) {
if let Some(diagnostic) = if let Some(diagnostic) =
@ -667,21 +665,17 @@ where
} }
if !self.is_stub { if !self.is_stub {
if self.enabled(Rule::DjangoModelWithoutDunderStr) { if self.enabled(Rule::DjangoModelWithoutDunderStr) {
if let Some(diagnostic) = flake8_django::rules::model_without_dunder_str(self, class_def);
flake8_django::rules::model_without_dunder_str(self, bases, body, stmt)
{
self.diagnostics.push(diagnostic);
}
} }
} }
if self.enabled(Rule::GlobalStatement) { if self.enabled(Rule::GlobalStatement) {
pylint::rules::global_statement(self, name); pylint::rules::global_statement(self, name);
} }
if self.enabled(Rule::UselessObjectInheritance) { 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) { 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 self.enabled(Rule::AmbiguousClassName) {
if let Some(diagnostic) = if let Some(diagnostic) =
@ -1370,6 +1364,51 @@ where
self.diagnostics.push(diagnostic); 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 { Stmt::Assert(ast::StmtAssert {
test, test,
@ -1409,7 +1448,7 @@ where
Stmt::With(ast::StmtWith { items, body, .. }) Stmt::With(ast::StmtWith { items, body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => { | Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => {
if self.enabled(Rule::AssertRaisesException) { 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) { if self.enabled(Rule::PytestRaisesWithMultipleStatements) {
flake8_pytest_style::rules::complex_raises(self, stmt, items, body); flake8_pytest_style::rules::complex_raises(self, stmt, items, body);
@ -1490,6 +1529,12 @@ where
if self.enabled(Rule::IncorrectDictIterator) { if self.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(self, target, iter); 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) { if self.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(self, iter); perflint::rules::unnecessary_list_cast(self, iter);
} }
@ -1528,9 +1573,7 @@ where
pyupgrade::rules::os_error_alias_handlers(self, handlers); pyupgrade::rules::os_error_alias_handlers(self, handlers);
} }
if self.enabled(Rule::PytestAssertInExcept) { if self.enabled(Rule::PytestAssertInExcept) {
self.diagnostics.extend( flake8_pytest_style::rules::assert_in_exception_handler(self, handlers);
flake8_pytest_style::rules::assert_in_exception_handler(handlers),
);
} }
if self.enabled(Rule::SuppressibleException) { if self.enabled(Rule::SuppressibleException) {
flake8_simplify::rules::suppressible_exception( flake8_simplify::rules::suppressible_exception(
@ -1571,11 +1614,7 @@ where
flake8_bugbear::rules::assignment_to_os_environ(self, targets); flake8_bugbear::rules::assignment_to_os_environ(self, targets);
} }
if self.enabled(Rule::HardcodedPasswordString) { if self.enabled(Rule::HardcodedPasswordString) {
if let Some(diagnostic) = flake8_bandit::rules::assign_hardcoded_password_string(self, value, targets);
flake8_bandit::rules::assign_hardcoded_password_string(value, targets)
{
self.diagnostics.push(diagnostic);
}
} }
if self.enabled(Rule::GlobalStatement) { if self.enabled(Rule::GlobalStatement) {
for target in targets.iter() { for target in targets.iter() {
@ -2107,6 +2146,7 @@ where
&& self.settings.target_version >= PythonVersion::Py37 && self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations() && !self.semantic.future_annotations()
&& self.semantic.in_annotation() && self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{ {
flake8_future_annotations::rules::future_rewritable_type_annotation( flake8_future_annotations::rules::future_rewritable_type_annotation(
self, value, self, value,
@ -2117,7 +2157,8 @@ where
if self.settings.target_version >= PythonVersion::Py310 if self.settings.target_version >= PythonVersion::Py310
|| (self.settings.target_version >= PythonVersion::Py37 || (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations() && self.semantic.future_annotations()
&& self.semantic.in_annotation()) && self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
{ {
pyupgrade::rules::use_pep604_annotation( pyupgrade::rules::use_pep604_annotation(
self, expr, slice, operator, self, expr, slice, operator,
@ -2193,6 +2234,9 @@ where
if self.enabled(Rule::NumpyDeprecatedTypeAlias) { if self.enabled(Rule::NumpyDeprecatedTypeAlias) {
numpy::rules::deprecated_type_alias(self, expr); numpy::rules::deprecated_type_alias(self, expr);
} }
if self.enabled(Rule::NumpyDeprecatedFunction) {
numpy::rules::deprecated_function(self, expr);
}
if self.is_stub { if self.is_stub {
if self.enabled(Rule::CollectionsNamedTuple) { if self.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(self, expr); flake8_pyi::rules::collections_named_tuple(self, expr);
@ -2212,6 +2256,7 @@ where
&& self.settings.target_version >= PythonVersion::Py37 && self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations() && !self.semantic.future_annotations()
&& self.semantic.in_annotation() && self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{ {
flake8_future_annotations::rules::future_rewritable_type_annotation( flake8_future_annotations::rules::future_rewritable_type_annotation(
self, expr, self, expr,
@ -2222,7 +2267,8 @@ where
if self.settings.target_version >= PythonVersion::Py39 if self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37 || (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations() && self.semantic.future_annotations()
&& self.semantic.in_annotation()) && self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
{ {
pyupgrade::rules::use_pep585_annotation( pyupgrade::rules::use_pep585_annotation(
self, self,
@ -2287,6 +2333,7 @@ where
&& self.settings.target_version >= PythonVersion::Py37 && self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations() && !self.semantic.future_annotations()
&& self.semantic.in_annotation() && self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{ {
flake8_future_annotations::rules::future_rewritable_type_annotation( flake8_future_annotations::rules::future_rewritable_type_annotation(
self, expr, self, expr,
@ -2297,7 +2344,8 @@ where
if self.settings.target_version >= PythonVersion::Py39 if self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37 || (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations() && 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); pyupgrade::rules::use_pep585_annotation(self, expr, &replacement);
} }
@ -2315,6 +2363,9 @@ where
if self.enabled(Rule::NumpyDeprecatedTypeAlias) { if self.enabled(Rule::NumpyDeprecatedTypeAlias) {
numpy::rules::deprecated_type_alias(self, expr); numpy::rules::deprecated_type_alias(self, expr);
} }
if self.enabled(Rule::NumpyDeprecatedFunction) {
numpy::rules::deprecated_function(self, expr);
}
if self.enabled(Rule::DeprecatedMockImport) { if self.enabled(Rule::DeprecatedMockImport) {
pyupgrade::rules::deprecated_mock_attribute(self, expr); pyupgrade::rules::deprecated_mock_attribute(self, expr);
} }
@ -2533,9 +2584,7 @@ where
flake8_pie::rules::unnecessary_dict_kwargs(self, expr, keywords); flake8_pie::rules::unnecessary_dict_kwargs(self, expr, keywords);
} }
if self.enabled(Rule::ExecBuiltin) { if self.enabled(Rule::ExecBuiltin) {
if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) { flake8_bandit::rules::exec_used(self, func);
self.diagnostics.push(diagnostic);
}
} }
if self.enabled(Rule::BadFilePermissions) { if self.enabled(Rule::BadFilePermissions) {
flake8_bandit::rules::bad_file_permissions(self, func, args, keywords); 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); flake8_bandit::rules::jinja2_autoescape_false(self, func, args, keywords);
} }
if self.enabled(Rule::HardcodedPasswordFuncArg) { if self.enabled(Rule::HardcodedPasswordFuncArg) {
self.diagnostics flake8_bandit::rules::hardcoded_password_func_arg(self, keywords);
.extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords));
} }
if self.enabled(Rule::HardcodedSQLExpression) { if self.enabled(Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr); flake8_bandit::rules::hardcoded_sql_expression(self, expr);
@ -2692,17 +2740,12 @@ where
flake8_debugger::rules::debugger_call(self, expr, func); flake8_debugger::rules::debugger_call(self, expr, func);
} }
if self.enabled(Rule::PandasUseOfInplaceArgument) { if self.enabled(Rule::PandasUseOfInplaceArgument) {
self.diagnostics.extend( pandas_vet::rules::inplace_argument(self, expr, func, args, keywords);
pandas_vet::rules::inplace_argument(self, expr, func, args, keywords)
.into_iter(),
);
} }
pandas_vet::rules::call(self, func); pandas_vet::rules::call(self, func);
if self.enabled(Rule::PandasUseOfPdMerge) { if self.enabled(Rule::PandasUseOfPdMerge) {
if let Some(diagnostic) = pandas_vet::rules::use_of_pd_merge(func) { pandas_vet::rules::use_of_pd_merge(self, func);
self.diagnostics.push(diagnostic);
};
} }
if self.enabled(Rule::CallDatetimeWithoutTzinfo) { if self.enabled(Rule::CallDatetimeWithoutTzinfo) {
flake8_datetimez::rules::call_datetime_without_tzinfo( flake8_datetimez::rules::call_datetime_without_tzinfo(
@ -2819,16 +2862,13 @@ where
&self.settings.flake8_gettext.functions_names, &self.settings.flake8_gettext.functions_names,
) { ) {
if self.enabled(Rule::FStringInGetTextFuncCall) { if self.enabled(Rule::FStringInGetTextFuncCall) {
self.diagnostics flake8_gettext::rules::f_string_in_gettext_func_call(self, args);
.extend(flake8_gettext::rules::f_string_in_gettext_func_call(args));
} }
if self.enabled(Rule::FormatInGetTextFuncCall) { if self.enabled(Rule::FormatInGetTextFuncCall) {
self.diagnostics flake8_gettext::rules::format_in_gettext_func_call(self, args);
.extend(flake8_gettext::rules::format_in_gettext_func_call(args));
} }
if self.enabled(Rule::PrintfInGetTextFuncCall) { if self.enabled(Rule::PrintfInGetTextFuncCall) {
self.diagnostics flake8_gettext::rules::printf_in_gettext_func_call(self, args);
.extend(flake8_gettext::rules::printf_in_gettext_func_call(args));
} }
} }
if self.enabled(Rule::UncapitalizedEnvironmentVariables) { if self.enabled(Rule::UncapitalizedEnvironmentVariables) {
@ -2869,7 +2909,7 @@ where
flake8_use_pathlib::rules::replaceable_by_pathlib(self, func); flake8_use_pathlib::rules::replaceable_by_pathlib(self, func);
} }
if self.enabled(Rule::NumpyLegacyRandom) { if self.enabled(Rule::NumpyLegacyRandom) {
numpy::rules::numpy_legacy_random(self, func); numpy::rules::legacy_random(self, func);
} }
if self.any_enabled(&[ if self.any_enabled(&[
Rule::LoggingStringFormat, Rule::LoggingStringFormat,
@ -3169,11 +3209,10 @@ where
flake8_2020::rules::compare(self, left, ops, comparators); flake8_2020::rules::compare(self, left, ops, comparators);
} }
if self.enabled(Rule::HardcodedPasswordString) { if self.enabled(Rule::HardcodedPasswordString) {
self.diagnostics.extend(
flake8_bandit::rules::compare_to_hardcoded_password_string( flake8_bandit::rules::compare_to_hardcoded_password_string(
self,
left, left,
comparators, comparators,
),
); );
} }
if self.enabled(Rule::ComparisonWithItself) { if self.enabled(Rule::ComparisonWithItself) {
@ -3194,29 +3233,6 @@ where
if self.enabled(Rule::YodaConditions) { if self.enabled(Rule::YodaConditions) {
flake8_simplify::rules::yoda_conditions(self, expr, left, ops, comparators); 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 { Expr::Constant(ast::ExprConstant {
value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. }, value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. },
@ -4422,7 +4438,7 @@ impl<'a> Checker<'a> {
} }
fn handle_node_delete(&mut self, expr: &'a Expr) { 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; return;
}; };

View File

@ -596,6 +596,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-pyi // flake8-pyi
(Flake8Pyi, "001") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnprefixedTypeParam), (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, "006") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadVersionInfoComparison),
(Flake8Pyi, "007") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformCheck), (Flake8Pyi, "007") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformCheck),
(Flake8Pyi, "008") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformName), (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
(Numpy, "001") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedTypeAlias), (Numpy, "001") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedTypeAlias),
(Numpy, "002") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyLegacyRandom), (Numpy, "002") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyLegacyRandom),
(Numpy, "003") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedFunction),
// ruff // ruff
(Ruff, "001") => (RuleGroup::Unspecified, rules::ruff::rules::AmbiguousUnicodeCharacterString), (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, "011") => (RuleGroup::Unspecified, rules::ruff::rules::StaticKeyDictComprehension),
(Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault), (Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault),
(Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional), (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, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml), (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, "101") => (RuleGroup::Unspecified, rules::perflint::rules::UnnecessaryListCast),
(Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator), (Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator),
(Perflint, "203") => (RuleGroup::Unspecified, rules::perflint::rules::TryExceptInLoop), (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 // flake8-fixme
(Flake8Fixme, "001") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsFixme), (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 ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter; 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::styles::SectionStyle;
use crate::docstrings::{Docstring, DocstringBody}; use crate::docstrings::{Docstring, DocstringBody};
@ -144,15 +144,13 @@ impl<'a> SectionContexts<'a> {
let mut contexts = Vec::new(); let mut contexts = Vec::new();
let mut last: Option<SectionContextData> = None; let mut last: Option<SectionContextData> = None;
let mut previous_line = None;
for line in contents.universal_newlines() { let mut lines = contents.universal_newlines().peekable();
if previous_line.is_none() {
// skip the first line
previous_line = Some(line.as_str());
continue;
}
// 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) { if let Some(section_kind) = suspected_as_section(&line, style) {
let indent = leading_space(&line); let indent = leading_space(&line);
let section_name = leading_words(&line); let section_name = leading_words(&line);
@ -162,7 +160,8 @@ impl<'a> SectionContexts<'a> {
if is_docstring_section( if is_docstring_section(
&line, &line,
section_name_range, section_name_range,
previous_line.unwrap_or_default(), previous_line.as_ref(),
lines.peek(),
) { ) {
if let Some(mut last) = last.take() { if let Some(mut last) = last.take() {
last.range = TextRange::new(last.range.start(), line.start()); 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() { 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. /// 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 section_name_suffix = line[usize::from(section_name_range.end())..].trim();
let this_looks_like_a_section_name = let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty(); 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; return false;
} }
let prev_line = previous_lines.trim(); // Determine whether the next line is an underline, e.g., "-----".
let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')'] 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() .into_iter()
.any(|char| prev_line.ends_with(char)); .any(|char| previous_line.ends_with(char));
let prev_line_looks_like_end_of_paragraph = previous_line_ends_with_punctuation || previous_line.is_empty()
prev_line_ends_with_punctuation || prev_line.is_empty(); });
if !prev_line_looks_like_end_of_paragraph { if !previous_line_looks_like_end_of_paragraph {
return false; return false;
} }

View File

@ -333,7 +333,7 @@ pub(crate) fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec
for selector in selectors { for selector in selectors {
if selector if selector
.into_iter() .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; return true;
} }

View File

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

View File

@ -19,7 +19,7 @@ impl Rule {
pub fn from_code(code: &str) -> Result<Self, FromCodeError> { pub fn from_code(code: &str) -> Result<Self, FromCodeError> {
let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?; let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?;
let prefix: RuleCodePrefix = RuleCodePrefix::parse(&linter, code)?; 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::fmt::{Debug, Formatter};
use std::iter::FusedIterator; use std::iter::FusedIterator;
const RULESET_SIZE: usize = 10; const RULESET_SIZE: usize = 11;
/// A set of [`Rule`]s. /// A set of [`Rule`]s.
/// ///

View File

@ -158,16 +158,16 @@ impl IntoIterator for &RuleSelector {
} }
RuleSelector::C => RuleSelectorIter::Chain( RuleSelector::C => RuleSelectorIter::Chain(
Linter::Flake8Comprehensions Linter::Flake8Comprehensions
.into_iter() .rules()
.chain(Linter::McCabe.into_iter()), .chain(Linter::McCabe.rules()),
), ),
RuleSelector::T => RuleSelectorIter::Chain( RuleSelector::T => RuleSelectorIter::Chain(
Linter::Flake8Debugger Linter::Flake8Debugger
.into_iter() .rules()
.chain(Linter::Flake8Print.into_iter()), .chain(Linter::Flake8Print.rules()),
), ),
RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.into_iter()), RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.rules()),
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.into_iter()), RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.clone().rules()),
} }
} }
} }
@ -346,7 +346,7 @@ mod clap_completion {
let prefix = p.linter().common_prefix(); let prefix = p.linter().common_prefix();
let code = p.short_code(); let code = p.short_code();
let mut rules_iter = p.into_iter(); let mut rules_iter = p.rules();
let rule1 = rules_iter.next(); let rule1 = rules_iter.next();
let rule2 = 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`. // TODO(charlie): Consider using the AST directly here rather than `Definition`.
// We could adhere more closely to `flake8-annotations` by defining public // We could adhere more closely to `flake8-annotations` by defining public
// vs. secret vs. protected. // vs. secret vs. protected.
let Definition::Member(Member { let Definition::Member(Member { kind, stmt, .. }) = definition else {
kind,
stmt,
..
}) = definition else {
return vec![]; return vec![];
}; };

View File

@ -39,6 +39,7 @@ mod tests {
#[test_case(Rule::SubprocessPopenWithShellEqualsTrue, Path::new("S602.py"))] #[test_case(Rule::SubprocessPopenWithShellEqualsTrue, Path::new("S602.py"))]
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))] #[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
#[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.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::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"))] #[test_case(Rule::TryExceptContinue, Path::new("S112.py"))]
#[test_case(Rule::TryExceptPass, Path::new("S110.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_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[violation] #[violation]
pub struct ExecBuiltin; pub struct ExecBuiltin;
@ -14,12 +16,16 @@ impl Violation for ExecBuiltin {
} }
/// S102 /// S102
pub(crate) fn exec_used(expr: &Expr, func: &Expr) -> Option<Diagnostic> { pub(crate) fn exec_used(checker: &mut Checker, func: &Expr) {
let Expr::Name(ast::ExprName { id, .. }) = func else { if checker
return None; .semantic()
}; .resolve_call_path(func)
if id != "exec" { .map_or(false, |call_path| {
return None; 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 rustpython_parser::ast::{Arg, ArgWithDefault, Arguments, Expr, Ranged};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
@ -36,9 +37,7 @@ fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option<Diagnostic> {
} }
/// S107 /// S107
pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec<Diagnostic> { pub(crate) fn hardcoded_password_default(checker: &mut Checker, arguments: &Arguments) {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
for ArgWithDefault { for ArgWithDefault {
def, def,
default, default,
@ -53,9 +52,7 @@ pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec<Diagnosti
continue; continue;
}; };
if let Some(diagnostic) = check_password_kwarg(def, default) { 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 rustpython_parser::ast::{Keyword, Ranged};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
@ -22,10 +23,10 @@ impl Violation for HardcodedPasswordFuncArg {
} }
/// S106 /// S106
pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec<Diagnostic> { pub(crate) fn hardcoded_password_func_arg(checker: &mut Checker, keywords: &[Keyword]) {
keywords checker
.iter() .diagnostics
.filter_map(|keyword| { .extend(keywords.iter().filter_map(|keyword| {
string_literal(&keyword.value).filter(|string| !string.is_empty())?; string_literal(&keyword.value).filter(|string| !string.is_empty())?;
let arg = keyword.arg.as_ref()?; let arg = keyword.arg.as_ref()?;
if !matches_password_name(arg) { if !matches_password_name(arg) {
@ -37,6 +38,5 @@ pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec<Diagnosti
}, },
keyword.range(), keyword.range(),
)) ))
}) }));
.collect()
} }

View File

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

View File

@ -67,7 +67,7 @@ fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Optio
return None; return None;
}; };
// Only evaluate the full BinOp, not the nested components. // 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) { if any_over_expr(expr, &has_string_literal) {
return Some(checker.generator().expr(expr)); 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 /// S501
pub(crate) fn request_with_no_cert_validation( pub(crate) fn request_with_no_cert_validation(
checker: &mut Checker, checker: &mut Checker,
@ -46,16 +31,13 @@ pub(crate) fn request_with_no_cert_validation(
if let Some(target) = checker if let Some(target) = checker
.semantic() .semantic()
.resolve_call_path(func) .resolve_call_path(func)
.and_then(|call_path| { .and_then(|call_path| match call_path.as_slice() {
if call_path.len() == 2 { ["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete"] => {
if call_path[0] == "requests" && REQUESTS_HTTP_VERBS.contains(&call_path[1]) { Some("requests")
return Some("requests");
} }
if call_path[0] == "httpx" && HTTPX_METHODS.contains(&call_path[1]) { ["httpx", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request"
return Some("httpx"); | "stream" | "Client" | "AsyncClient"] => Some("httpx"),
} _ => None,
}
None
}) })
{ {
let call_args = SimpleCallArgs::new(args, keywords); 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_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, 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; use crate::checkers::ast::Checker;
#[violation] #[violation]
pub struct RequestWithoutTimeout { pub struct RequestWithoutTimeout {
pub timeout: Option<String>, implicit: bool,
} }
impl Violation for RequestWithoutTimeout { impl Violation for RequestWithoutTimeout {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let RequestWithoutTimeout { timeout } = self; let RequestWithoutTimeout { implicit } = self;
match timeout { if *implicit {
Some(value) => { format!("Probable use of requests call without timeout")
format!("Probable use of requests call with timeout set to `{value}`") } else {
} format!("Probable use of requests call with timeout set to `None`")
None => format!("Probable use of requests call without timeout"),
} }
} }
} }
const HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"];
/// S113 /// S113
pub(crate) fn request_without_timeout( pub(crate) fn request_without_timeout(
checker: &mut Checker, checker: &mut Checker,
@ -37,30 +34,26 @@ pub(crate) fn request_without_timeout(
.semantic() .semantic()
.resolve_call_path(func) .resolve_call_path(func)
.map_or(false, |call_path| { .map_or(false, |call_path| {
HTTP_VERBS matches!(
.iter() call_path.as_slice(),
.any(|func_name| call_path.as_slice() == ["requests", func_name]) [
"requests",
"get" | "options" | "head" | "post" | "put" | "patch" | "delete"
]
)
}) })
{ {
let call_args = SimpleCallArgs::new(args, keywords); let call_args = SimpleCallArgs::new(args, keywords);
if let Some(timeout_arg) = call_args.keyword_argument("timeout") { if let Some(timeout) = call_args.keyword_argument("timeout") {
if let Some(timeout) = match timeout_arg { if is_const_none(timeout) {
Expr::Constant(ast::ExprConstant {
value: value @ Constant::None,
..
}) => Some(checker.generator().constant(value)),
_ => None,
} {
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { RequestWithoutTimeout { implicit: false },
timeout: Some(timeout), timeout.range(),
},
timeout_arg.range(),
)); ));
} }
} else { } else {
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { timeout: None }, RequestWithoutTimeout { implicit: true },
func.range(), 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) { pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) {
let Expr::Call(ast::ExprCall { func, .. }) = expr else { let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return; return;
@ -246,7 +246,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) {
// Mktemp // Mktemp
["tempfile", "mktemp"] => Some(SuspiciousMktempUsage.into()), ["tempfile", "mktemp"] => Some(SuspiciousMktempUsage.into()),
// Eval // Eval
["eval"] => Some(SuspiciousEvalUsage.into()), ["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()),
// MarkSafe // MarkSafe
["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()), ["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
// URLOpen // URLOpen

View File

@ -6,7 +6,7 @@ S102.py:3:5: S102 Use of `exec` detected
1 | def fn(): 1 | def fn():
2 | # Error 2 | # Error
3 | exec('x = 2') 3 | exec('x = 2')
| ^^^^^^^^^^^^^ S102 | ^^^^ S102
4 | 4 |
5 | exec('y = 3') 5 | exec('y = 3')
| |
@ -16,7 +16,7 @@ S102.py:5:1: S102 Use of `exec` detected
3 | exec('x = 2') 3 | exec('x = 2')
4 | 4 |
5 | exec('y = 3') 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; continue;
} }
let ( let (Stmt::FunctionDef(ast::StmtFunctionDef {
Stmt::FunctionDef(ast::StmtFunctionDef {
decorator_list,
body,
name: method_name,
..
}) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
decorator_list, decorator_list,
body, body,
name: method_name, name: method_name,
.. ..
}) })
) = stmt else { | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
decorator_list,
body,
name: method_name,
..
})) = stmt
else {
continue; 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_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum AssertionKind {
AssertRaises,
PytestRaises,
}
/// ## What it does /// ## 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? /// ## Why is this bad?
/// These forms catch every `Exception`, which can lead to tests passing even /// 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 /// Either assert for a more specific exception (builtin or custom), or use
/// `assertRaisesRegex` or `pytest.raises(..., match=<REGEX>)` respectively. /// `assertRaisesRegex` or `pytest.raises(..., match=<REGEX>)` respectively.
@ -32,30 +30,61 @@ pub(crate) enum AssertionKind {
/// ``` /// ```
#[violation] #[violation]
pub struct AssertRaisesException { pub struct AssertRaisesException {
kind: AssertionKind, assertion: AssertionKind,
exception: ExceptionKind,
} }
impl Violation for AssertRaisesException { impl Violation for AssertRaisesException {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
match self.kind { let AssertRaisesException {
AssertionKind::AssertRaises => { assertion,
format!("`assertRaises(Exception)` should be considered evil") 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 /// B017
pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[WithItem]) { pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) {
let Some(item) = items.first() else { for item in items {
return; let Expr::Call(ast::ExprCall {
}; func,
let item_context = &item.context_expr; args,
let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &item_context else { keywords,
range: _,
}) = &item.context_expr
else {
return; return;
}; };
if args.len() != 1 { if args.len() != 1 {
@ -65,18 +94,19 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items:
return; return;
} }
if !checker let Some(exception) = checker
.semantic() .semantic()
.resolve_call_path(args.first().unwrap()) .resolve_call_path(args.first().unwrap())
.map_or(false, |call_path| { .and_then(|call_path| match call_path.as_slice() {
matches!(call_path.as_slice(), ["", "Exception"]) ["", "Exception"] => Some(ExceptionKind::Exception),
["", "BaseException"] => Some(ExceptionKind::BaseException),
_ => None,
}) })
{ else {
return; return;
} };
let kind = { let assertion = if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
{ {
AssertionKind::AssertRaises AssertionKind::AssertRaises
} else if checker } else if checker
@ -92,11 +122,14 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items:
AssertionKind::PytestRaises AssertionKind::PytestRaises
} else { } else {
return; return;
}
}; };
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
AssertRaisesException { kind }, AssertRaisesException {
stmt.range(), 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" { if attr != "environ" {
return; return;
} }
let Expr::Name(ast::ExprName { id, .. } )= value.as_ref() else { let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return; return;
}; };
if id != "os" { 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 seen: FxHashSet<CallPath> = FxHashSet::default();
let mut duplicates: FxHashMap<CallPath, Vec<&Expr>> = FxHashMap::default(); let mut duplicates: FxHashMap<CallPath, Vec<&Expr>> = FxHashMap::default();
for handler in handlers { 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; continue;
}; };
match type_.as_ref() { match type_.as_ref() {

View File

@ -47,7 +47,7 @@ impl Violation for ExceptWithNonExceptionClasses {
/// This should leave any unstarred iterables alone (subsequently raising a /// This should leave any unstarred iterables alone (subsequently raising a
/// warning for B029). /// warning for B029).
fn flatten_starred_iterables(expr: &Expr) -> Vec<&Expr> { 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]; return vec![expr];
}; };
let mut flattened_exprs: Vec<&Expr> = Vec::with_capacity(elts.len()); 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 rustpython_parser::ast::{self, Comprehension, Expr, ExprContext, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, 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::types::Node;
use ruff_python_ast::visitor; use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::Visitor;
@ -58,19 +57,17 @@ impl Violation for FunctionUsesLoopVariable {
#[derive(Default)] #[derive(Default)]
struct LoadedNamesVisitor<'a> { struct LoadedNamesVisitor<'a> {
// Tuple of: name, defining expression, and defining range. loaded: Vec<&'a ast::ExprName>,
loaded: Vec<(&'a str, &'a Expr)>, stored: Vec<&'a ast::ExprName>,
// Tuple of: name, defining expression, and defining range.
stored: Vec<(&'a str, &'a Expr)>,
} }
/// `Visitor` to collect all used identifiers in a statement. /// `Visitor` to collect all used identifiers in a statement.
impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> { impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) { fn visit_expr(&mut self, expr: &'a Expr) {
match expr { match expr {
Expr::Name(ast::ExprName { id, ctx, range: _ }) => match ctx { Expr::Name(name) => match &name.ctx {
ExprContext::Load => self.loaded.push((id, expr)), ExprContext::Load => self.loaded.push(name),
ExprContext::Store => self.stored.push((id, expr)), ExprContext::Store => self.stored.push(name),
ExprContext::Del => {} ExprContext::Del => {}
}, },
_ => visitor::walk_expr(self, expr), _ => visitor::walk_expr(self, expr),
@ -80,7 +77,7 @@ impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> {
#[derive(Default)] #[derive(Default)]
struct SuspiciousVariablesVisitor<'a> { struct SuspiciousVariablesVisitor<'a> {
names: Vec<(&'a str, &'a Expr)>, names: Vec<&'a ast::ExprName>,
safe_functions: Vec<&'a Expr>, safe_functions: Vec<&'a Expr>,
} }
@ -95,17 +92,20 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
let mut visitor = LoadedNamesVisitor::default(); let mut visitor = LoadedNamesVisitor::default();
visitor.visit_body(body); 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". // Treat any non-arguments as "suspicious".
self.names.extend( self.names
visitor .extend(visitor.loaded.into_iter().filter(|loaded| {
.loaded if visitor.stored.iter().any(|stored| stored.id == loaded.id) {
.into_iter() return false;
.filter(|(id, ..)| !arg_names.contains(id)), }
);
if includes_arg_name(&loaded.id, args) {
return false;
}
true
}));
return; return;
} }
Stmt::Return(ast::StmtReturn { Stmt::Return(ast::StmtReturn {
@ -132,10 +132,9 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
}) => { }) => {
match func.as_ref() { match func.as_ref() {
Expr::Name(ast::ExprName { id, .. }) => { Expr::Name(ast::ExprName { id, .. }) => {
let id = id.as_str(); if matches!(id.as_str(), "filter" | "reduce" | "map") {
if id == "filter" || id == "reduce" || id == "map" {
for arg in args { for arg in args {
if matches!(arg, Expr::Lambda(_)) { if arg.is_lambda_expr() {
self.safe_functions.push(arg); self.safe_functions.push(arg);
} }
} }
@ -159,7 +158,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
for keyword in keywords { for keyword in keywords {
if keyword.arg.as_ref().map_or(false, |arg| arg == "key") 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); self.safe_functions.push(&keyword.value);
} }
@ -175,17 +174,19 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
let mut visitor = LoadedNamesVisitor::default(); let mut visitor = LoadedNamesVisitor::default();
visitor.visit_expr(body); 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". // Treat any non-arguments as "suspicious".
self.names.extend( self.names
visitor .extend(visitor.loaded.into_iter().filter(|loaded| {
.loaded if visitor.stored.iter().any(|stored| stored.id == loaded.id) {
.iter() return false;
.filter(|(id, ..)| !arg_names.contains(id)), }
);
if includes_arg_name(&loaded.id, args) {
return false;
}
true
}));
return; return;
} }
@ -198,7 +199,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
#[derive(Default)] #[derive(Default)]
struct NamesFromAssignmentsVisitor<'a> { struct NamesFromAssignmentsVisitor<'a> {
names: FxHashSet<&'a str>, names: Vec<&'a str>,
} }
/// `Visitor` to collect all names used in an assignment expression. /// `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) { fn visit_expr(&mut self, expr: &'a Expr) {
match expr { match expr {
Expr::Name(ast::ExprName { id, .. }) => { Expr::Name(ast::ExprName { id, .. }) => {
self.names.insert(id.as_str()); self.names.push(id.as_str());
} }
Expr::Starred(ast::ExprStarred { value, .. }) => { Expr::Starred(ast::ExprStarred { value, .. }) => {
self.visit_expr(value); self.visit_expr(value);
@ -223,7 +224,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> {
#[derive(Default)] #[derive(Default)]
struct AssignedNamesVisitor<'a> { struct AssignedNamesVisitor<'a> {
names: FxHashSet<&'a str>, names: Vec<&'a str>,
} }
/// `Visitor` to collect all used identifiers in a statement. /// `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) { fn visit_expr(&mut self, expr: &'a Expr) {
if matches!(expr, Expr::Lambda(_)) { if expr.is_lambda_expr() {
// Don't recurse. // Don't recurse.
return; 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 // If a variable was used in a function or lambda body, and assigned in the
// loop, flag it. // loop, flag it.
for (name, expr) in suspicious_variables { for name in suspicious_variables {
if reassigned_in_loop.contains(name) { if reassigned_in_loop.contains(&name.id.as_str()) {
if !checker.flake8_bugbear_seen.contains(&expr) { if !checker.flake8_bugbear_seen.contains(&name) {
checker.flake8_bugbear_seen.push(expr); checker.flake8_bugbear_seen.push(name);
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
FunctionUsesLoopVariable { 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, func: &Expr,
args: &[Expr], args: &[Expr],
) { ) {
let Expr::Name(ast::ExprName { id, .. } )= func else { let Expr::Name(ast::ExprName { id, .. }) = func else {
return; return;
}; };
if id != "getattr" { if id != "getattr" {
@ -76,7 +76,8 @@ pub(crate) fn getattr_with_constant(
let Expr::Constant(ast::ExprConstant { let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value), value: Constant::Str(value),
.. ..
} )= arg else { }) = arg
else {
return; return;
}; };
if !is_identifier(value) { 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.args)
.chain(&arguments.kwonlyargs) .chain(&arguments.kwonlyargs)
{ {
let Some(default)= default else { let Some(default) = default else {
continue; continue;
}; };

View File

@ -59,7 +59,11 @@ pub(crate) fn redundant_tuple_in_exception_handler(
handlers: &[ExceptHandler], handlers: &[ExceptHandler],
) { ) {
for handler in handlers { 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; continue;
}; };
let Expr::Tuple(ast::ExprTuple { elts, .. }) = type_.as_ref() else { 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 { let Expr::Constant(ast::ExprConstant {
value: Constant::Str(name), value: Constant::Str(name),
.. ..
} )= name else { }) = name
else {
return; return;
}; };
if !is_identifier(name) { if !is_identifier(name) {

View File

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

View File

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

View File

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

View File

@ -63,8 +63,8 @@ pub(crate) fn unreliable_callable_check(
let Expr::Constant(ast::ExprConstant { let Expr::Constant(ast::ExprConstant {
value: Constant::Str(s), value: Constant::Str(s),
.. ..
}) = &args[1] else }) = &args[1]
{ else {
return; return;
}; };
if s != "__call__" { 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 /// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to
/// `itertools.cycle` or similar). /// `itertools.cycle` or similar).
fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool { 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; return false;
}; };

View File

@ -1,27 +1,38 @@
--- ---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs 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): 21 | class Foobar(unittest.TestCase):
22 | def evil_raises(self) -> None: 22 | def evil_raises(self) -> None:
23 | with self.assertRaises(Exception): 23 | with self.assertRaises(Exception):
| _________^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
24 | | raise Exception("Evil I say!") 24 | raise Exception("Evil I say!")
| |__________________________________________^ B017
25 |
26 | def context_manager_raises(self) -> None:
| |
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(): 26 | def also_evil_raises(self) -> None:
41 | with pytest.raises(Exception): 27 | with self.assertRaises(BaseException):
| _____^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
42 | | raise ValueError("Hello") 28 | raise Exception("Evil I say!")
| |_________________________________^ B017 |
43 |
44 | with pytest.raises(Exception, "hello"): 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 ...`. // Extract the (k, v) from `(k, v) for ...`.
let generator_exp = match_generator_exp(&arg.value)?; let generator_exp = match_generator_exp(&arg.value)?;
let tuple = match_tuple(&generator_exp.elt)?; 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"); 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 tuple = match_tuple(&list_comp.elt)?;
let [Element::Simple { let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..]
value: key, .. else {
}, Element::Simple { value, .. }] = &tuple.elements[..] else { bail!("Expected tuple with two elements"); }; bail!("Expected tuple with two elements");
};
tree = Expression::DictComp(Box::new(DictComp { tree = Expression::DictComp(Box::new(DictComp {
key: Box::new(key.clone()), 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 { let Some(Element::Simple { value: key, .. }) = &tuple.elements.get(0) else {
bail!( bail!("Expected tuple to contain a key as the first element");
"Expected tuple to contain a key as the first element"
);
}; };
let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) else { let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) else {
bail!( bail!("Expected tuple to contain a key as the second element");
"Expected tuple to contain a key as the second element"
);
}; };
(key, value) (key, value)
@ -1063,9 +1061,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
let call = match_call_mut(&mut tree)?; let call = match_call_mut(&mut tree)?;
let Expression::ListComp(list_comp) = &call.args[0].value else { let Expression::ListComp(list_comp) = &call.args[0].value else {
bail!( bail!("Expected Expression::ListComp");
"Expected Expression::ListComp"
);
}; };
let mut new_empty_lines = vec![]; let mut new_empty_lines = vec![];

View File

@ -66,11 +66,13 @@ pub(crate) fn unnecessary_comprehension_any_all(
if !keywords.is_empty() { if !keywords.is_empty() {
return; return;
} }
let Expr::Name(ast::ExprName { id, .. } )= func else { let Expr::Name(ast::ExprName { id, .. }) = func else {
return; return;
}; };
if (matches!(id.as_str(), "all" | "any")) && args.len() == 1 { 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; return;
}; };
if contains_await(elt) { if contains_await(elt) {

View File

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

View File

@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_dict(
args: &[Expr], args: &[Expr],
keywords: &[Keyword], 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; return;
}; };
if let Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) = argument { if let Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) = argument {

View File

@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_list(
args: &[Expr], args: &[Expr],
keywords: &[Keyword], 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; return;
}; };
if !checker.semantic().is_builtin("list") { if !checker.semantic().is_builtin("list") {

View File

@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_set(
args: &[Expr], args: &[Expr],
keywords: &[Keyword], 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; return;
}; };
if !checker.semantic().is_builtin("set") { if !checker.semantic().is_builtin("set") {

View File

@ -47,7 +47,9 @@ pub(crate) fn unnecessary_list_comprehension_dict(
args: &[Expr], args: &[Expr],
keywords: &[Keyword], 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; return;
}; };
if !checker.semantic().is_builtin("dict") { if !checker.semantic().is_builtin("dict") {

View File

@ -47,7 +47,9 @@ pub(crate) fn unnecessary_list_comprehension_set(
args: &[Expr], args: &[Expr],
keywords: &[Keyword], 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; return;
}; };
if !checker.semantic().is_builtin("set") { if !checker.semantic().is_builtin("set") {

View File

@ -54,7 +54,9 @@ pub(crate) fn unnecessary_literal_dict(
args: &[Expr], args: &[Expr],
keywords: &[Keyword], 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; return;
}; };
if !checker.semantic().is_builtin("dict") { if !checker.semantic().is_builtin("dict") {

View File

@ -55,7 +55,9 @@ pub(crate) fn unnecessary_literal_set(
args: &[Expr], args: &[Expr],
keywords: &[Keyword], 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; return;
}; };
if !checker.semantic().is_builtin("set") { if !checker.semantic().is_builtin("set") {

View File

@ -127,7 +127,9 @@ pub(crate) fn unnecessary_map(
if args.len() != 2 { if args.len() != 2 {
return; 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; return;
}; };
if let Expr::Lambda(_) = argument { if let Expr::Lambda(_) = argument {
@ -155,7 +157,9 @@ pub(crate) fn unnecessary_map(
if args.len() == 1 { if args.len() == 1 {
if let Expr::Call(ast::ExprCall { func, args, .. }) = &args[0] { 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; return;
}; };
if let Expr::Lambda(ast::ExprLambda { body, .. }) = argument { 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 { let Expr::Subscript(ast::ExprSubscript { slice, .. }) = first_arg else {
return; 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; return;
}; };
if lower.is_some() || upper.is_some() { if lower.is_some() || upper.is_some() {
@ -77,13 +83,15 @@ pub(crate) fn unnecessary_subscript_reversal(
op: UnaryOp::USub, op: UnaryOp::USub,
operand, operand,
range: _, range: _,
}) = step.as_ref() else { }) = step.as_ref()
else {
return; return;
}; };
let Expr::Constant(ast::ExprConstant { let Expr::Constant(ast::ExprConstant {
value: Constant::Int(val), value: Constant::Int(val),
.. ..
}) = operand.as_ref() else { }) = operand.as_ref()
else {
return; return;
}; };
if *val != BigInt::from(1) { 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 { let (Some(grandparent), Some(parent)) = (
checker.diagnostics.push(Diagnostic::new( checker.semantic().expr_grandparent(),
CallDatetimeStrptimeWithoutZone, checker.semantic().expr_parent(),
location, ) else {
)); checker
.diagnostics
.push(Diagnostic::new(CallDatetimeStrptimeWithoutZone, location));
return; return;
}; };

View File

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

View File

@ -52,21 +52,20 @@ impl Violation for DjangoModelWithoutDunderStr {
/// DJ008 /// DJ008
pub(crate) fn model_without_dunder_str( pub(crate) fn model_without_dunder_str(
checker: &Checker, checker: &mut Checker,
bases: &[Expr], ast::StmtClassDef {
body: &[Stmt], name, bases, body, ..
class_location: &Stmt, }: &ast::StmtClassDef,
) -> Option<Diagnostic> { ) {
if !is_non_abstract_model(bases, body, checker.semantic()) { if !is_non_abstract_model(bases, body, checker.semantic()) {
return None; return;
} }
if !has_dunder_method(body) { if has_dunder_method(body) {
return Some(Diagnostic::new( return;
DjangoModelWithoutDunderStr,
class_location.range(),
));
} }
None checker
.diagnostics
.push(Diagnostic::new(DjangoModelWithoutDunderStr, name.range()));
} }
fn has_dunder_method(body: &[Stmt]) -> bool { 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. /// Check if class is abstract, in terms of Django model inheritance.
fn is_model_abstract(body: &[Stmt]) -> bool { fn is_model_abstract(body: &[Stmt]) -> bool {
for element in body.iter() { for element in body.iter() {
let Stmt::ClassDef(ast::StmtClassDef {name, body, ..}) = element else { let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
continue continue;
}; };
if name != "Meta" { if name != "Meta" {
continue; continue;
} }
for element in body.iter() { for element in body.iter() {
let Stmt::Assign(ast::StmtAssign {targets, value, ..}) = element else { let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = element else {
continue; continue;
}; };
for target in targets.iter() { for target in targets.iter() {
let Expr::Name(ast::ExprName {id , ..}) = target else { let Expr::Name(ast::ExprName { id, .. }) = target else {
continue; continue;
}; };
if id != "abstract" { 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_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use crate::checkers::ast::Checker;
/// ## What it does /// ## What it does
/// Checks that Django's `@receiver` decorator is listed first, prior to /// Checks that Django's `@receiver` decorator is listed first, prior to
@ -48,25 +49,19 @@ impl Violation for DjangoNonLeadingReceiverDecorator {
} }
/// DJ013 /// DJ013
pub(crate) fn non_leading_receiver_decorator<'a, F>( pub(crate) fn non_leading_receiver_decorator(checker: &mut Checker, decorator_list: &[Decorator]) {
decorator_list: &'a [Decorator],
resolve_call_path: F,
) -> Vec<Diagnostic>
where
F: Fn(&'a Expr) -> Option<CallPath<'a>>,
{
let mut diagnostics = vec![];
let mut seen_receiver = false; let mut seen_receiver = false;
for (i, decorator) in decorator_list.iter().enumerate() { for (i, decorator) in decorator_list.iter().enumerate() {
let is_receiver = match &decorator.expression { let is_receiver = decorator.expression.as_call_expr().map_or(false, |call| {
Expr::Call(ast::ExprCall { func, .. }) => resolve_call_path(func) checker
.semantic()
.resolve_call_path(&call.func)
.map_or(false, |call_path| { .map_or(false, |call_path| {
matches!(call_path.as_slice(), ["django", "dispatch", "receiver"]) matches!(call_path.as_slice(), ["django", "dispatch", "receiver"])
}), })
_ => false, });
};
if i > 0 && is_receiver && !seen_receiver { if i > 0 && is_receiver && !seen_receiver {
diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
DjangoNonLeadingReceiverDecorator, DjangoNonLeadingReceiverDecorator,
decorator.range(), decorator.range(),
)); ));
@ -77,5 +72,4 @@ where
seen_receiver = true; 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 /// DJ001
pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> Vec<Diagnostic> { pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt]) {
let mut errors = Vec::new();
for statement in body.iter() { for statement in body.iter() {
let Stmt::Assign(ast::StmtAssign {value, ..}) = statement else { let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else {
continue continue;
}; };
if let Some(field_name) = is_nullable_field(checker, value) { if let Some(field_name) = is_nullable_field(checker, value) {
errors.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
DjangoNullableModelStringField { DjangoNullableModelStringField {
field_name: field_name.to_string(), 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> { 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; return None;
}; };
@ -88,7 +77,10 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st
return None; return None;
}; };
if !NOT_NULL_TRUE_FIELDS.contains(&valid_field_name) { if !matches!(
valid_field_name,
"CharField" | "TextField" | "SlugField" | "EmailField" | "FilePathField" | "URLField"
) {
return None; 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; let mut unique_key = false;
for keyword in keywords.iter() { for keyword in keywords.iter() {
let Some(argument) = &keyword.arg else { let Some(argument) = &keyword.arg else {
continue continue;
}; };
if !is_const_true(&keyword.value) { if !is_const_true(&keyword.value) {
continue; 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 /// [Django Style Guide]: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#model-style
#[violation] #[violation]
pub struct DjangoUnorderedBodyContentInModel { pub struct DjangoUnorderedBodyContentInModel {
elem_type: ContentType, element_type: ContentType,
before: ContentType, prev_element_type: ContentType,
} }
impl Violation for DjangoUnorderedBodyContentInModel { impl Violation for DjangoUnorderedBodyContentInModel {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let DjangoUnorderedBodyContentInModel { elem_type, before } = self; let DjangoUnorderedBodyContentInModel {
format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {elem_type} should come before {before}") 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)] #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub(crate) enum ContentType { enum ContentType {
FieldDeclaration, FieldDeclaration,
ManagerDeclaration, ManagerDeclaration,
MetaClass, MetaClass,
@ -149,24 +152,38 @@ pub(crate) fn unordered_body_content_in_model(
{ {
return; 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() { 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; 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() .iter()
.find(|&&element_type| element_type > current_element_type) else { .find(|&&prev_element_type| prev_element_type > element_type)
elements_type_found.push(current_element_type); {
continue;
};
let diagnostic = Diagnostic::new( let diagnostic = Diagnostic::new(
DjangoUnorderedBodyContentInModel { DjangoUnorderedBodyContentInModel {
elem_type: current_element_type, element_type,
before: element_type, prev_element_type,
}, },
element.range(), element.range(),
); );
checker.diagnostics.push(diagnostic); 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 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__ 5 | # Models without __str__
6 | / class TestModel1(models.Model): 6 | class TestModel1(models.Model):
7 | | new_field = models.CharField(max_length=10) | ^^^^^^^^^^ DJ008
8 | | 7 | new_field = models.CharField(max_length=10)
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
| |
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): 21 | class TestModel2(Model):
22 | | new_field = models.CharField(max_length=10) | ^^^^^^^^^^ DJ008
23 | | 22 | new_field = models.CharField(max_length=10)
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
| |
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): 36 | class TestModel3(Model):
37 | | new_field = models.CharField(max_length=10) | ^^^^^^^^^^ DJ008
38 | | 37 | new_field = models.CharField(max_length=10)
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
| |

View File

@ -37,4 +37,21 @@ DJ012.py:69:5: DJ012 Order of model's inner classes, methods, and fields does no
| |____________^ DJ012 | |____________^ 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_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[violation] #[violation]
pub struct FStringInGetTextFuncCall; pub struct FStringInGetTextFuncCall;
@ -14,11 +16,12 @@ impl Violation for FStringInGetTextFuncCall {
} }
/// INT001 /// 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 let Some(first) = args.first() {
if first.is_joined_str_expr() { 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_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[violation] #[violation]
pub struct FormatInGetTextFuncCall; pub struct FormatInGetTextFuncCall;
@ -14,15 +16,16 @@ impl Violation for FormatInGetTextFuncCall {
} }
/// INT002 /// 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 Some(first) = args.first() {
if let Expr::Call(ast::ExprCall { func, .. }) = &first { if let Expr::Call(ast::ExprCall { func, .. }) = &first {
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() {
if attr == "format" { 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 rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
@ -14,7 +15,7 @@ impl Violation for PrintfInGetTextFuncCall {
} }
/// INT003 /// 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 Some(first) = args.first() {
if let Expr::BinOp(ast::ExprBinOp { if let Expr::BinOp(ast::ExprBinOp {
op: Operator::Mod { .. }, op: Operator::Mod { .. },
@ -27,9 +28,10 @@ pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option<Diagnostic> {
.. ..
}) = left.as_ref() }) = 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 /// PIE810
pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { 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; return;
}; };
@ -70,24 +75,25 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
func, func,
args, args,
keywords, keywords,
range: _ range: _,
}) = &call else { }) = &call
continue else {
continue;
}; };
if !(args.len() == 1 && keywords.is_empty()) { if !(args.len() == 1 && keywords.is_empty()) {
continue; continue;
} }
let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func.as_ref() else { let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else {
continue continue;
}; };
if attr != "startswith" && attr != "endswith" { if attr != "startswith" && attr != "endswith" {
continue; continue;
} }
let Expr::Name(ast::ExprName { id: arg_name, .. } )= value.as_ref() else { let Expr::Name(ast::ExprName { id: arg_name, .. }) = value.as_ref() else {
continue continue;
}; };
duplicates duplicates
@ -110,8 +116,17 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
.iter() .iter()
.map(|index| &values[*index]) .map(|index| &values[*index])
.map(|expr| { .map(|expr| {
let Expr::Call(ast::ExprCall { func: _, args, keywords: _, range: _}) = expr else { let Expr::Call(ast::ExprCall {
unreachable!("{}", format!("Indices should only contain `{attr_name}` calls")) func: _,
args,
keywords: _,
range: _,
}) = expr
else {
unreachable!(
"{}",
format!("Indices should only contain `{attr_name}` calls")
)
}; };
args.get(0) args.get(0)
.unwrap_or_else(|| panic!("`{attr_name}` should have one argument")) .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::BadVersionInfoComparison, Path::new("PYI006.pyi"))]
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))] #[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))]
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))] #[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.py"))]
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))]
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))] #[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::TSuffixedTypeAlias, Path::new("PYI043.pyi"))]
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.py"))] #[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.py"))]
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))] #[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.py"))]
#[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))]
#[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))] #[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::UnrecognizedPlatformCheck, Path::new("PYI007.pyi"))]
#[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.py"))] #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.py"))]
#[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.pyi"))] #[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<()> { fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path( 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_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
@ -52,19 +52,23 @@ pub struct BadVersionInfoComparison;
impl Violation for BadVersionInfoComparison { impl Violation for BadVersionInfoComparison {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
format!("Use `<` or `>=` for version info comparisons") format!("Use `<` or `>=` for `sys.version_info` comparisons")
} }
} }
/// PYI006 /// PYI006
pub(crate) fn bad_version_info_comparison( pub(crate) fn bad_version_info_comparison(checker: &mut Checker, test: &Expr) {
checker: &mut Checker, let Expr::Compare(ast::ExprCompare {
expr: &Expr, left,
left: &Expr, ops,
ops: &[CmpOp], comparators,
comparators: &[Expr], ..
) { }) = test
let ([op], [_right]) = (ops, comparators) else { else {
return;
};
let ([op], [_right]) = (ops.as_slice(), comparators.as_slice()) else {
return; return;
}; };
@ -78,8 +82,11 @@ pub(crate) fn bad_version_info_comparison(
return; return;
} }
if !matches!(op, CmpOp::Lt | CmpOp::GtE) { if matches!(op, CmpOp::Lt | CmpOp::GtE) {
let diagnostic = Diagnostic::new(BadVersionInfoComparison, expr.range()); return;
checker.diagnostics.push(diagnostic);
} }
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, kind: MemberKind::Method,
stmt, stmt,
.. ..
}) = definition else { }) = definition
else {
return; return;
}; };
let Stmt::FunctionDef(ast::StmtFunctionDef { let Stmt::FunctionDef(ast::StmtFunctionDef { name, returns, .. }) = stmt else {
name,
returns,
..
}) = stmt else {
return; return;
}; };

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