diff --git a/.gitattributes b/.gitattributes index 5bb8d8b736..c4b5fa0751 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ crates/ruff/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf ruff.schema.json linguist-generated=true text=auto eol=lf +*.md.snap linguist-language=Markdown diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 7ae6cfdf97..6f1c0ae85f 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,5 +1,31 @@ # Breaking Changes +## 0.0.276 + +### The `keep-runtime-typing` setting has been reinstated ([#5470](https://github.com/astral-sh/ruff/pull/5470)) + +The `keep-runtime-typing` setting has been reinstated with revised semantics. This setting was +removed in [#4427](https://github.com/astral-sh/ruff/pull/4427), as it was equivalent to ignoring +the `UP006` and `UP007` rules via Ruff's standard `ignore` mechanism. + +Taking `UP006` (rewrite `List[int]` to `list[int]`) as an example, the setting now behaves as +follows: + +- On Python 3.7 and Python 3.8, setting `keep-runtime-typing = true` will cause Ruff to ignore + `UP006` violations, even if `from __future__ import annotations` is present in the file. + While such annotations are valid in Python 3.7 and Python 3.8 when combined with + `from __future__ import annotations`, they aren't supported by libraries like Pydantic and + FastAPI, which rely on runtime type checking. +- On Python 3.9 and above, the setting has no effect, as `list[int]` is a valid type annotation, + and libraries like Pydantic and FastAPI support it without issue. + +In short: `keep-runtime-typing` can be used to ensure that Ruff doesn't introduce type annotations +that are not supported at runtime by the current Python version, which are unsupported by libraries +like Pydantic and FastAPI. + +Note that this is not a breaking change, but is included here to complement the previous removal +of `keep-runtime-typing`. + ## 0.0.268 ### The `keep-runtime-typing` setting has been removed ([#4427](https://github.com/astral-sh/ruff/pull/4427)) diff --git a/Cargo.lock b/Cargo.lock index 9e7fbc656e..efc811ad0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "flake8-to-ruff" -version = "0.0.275" +version = "0.0.276" dependencies = [ "anyhow", "clap", @@ -1829,7 +1829,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.0.275" +version = "0.0.276" dependencies = [ "annotate-snippets 0.9.1", "anyhow", @@ -1865,6 +1865,7 @@ dependencies = [ "result-like", "ruff_cache", "ruff_diagnostics", + "ruff_index", "ruff_macros", "ruff_python_ast", "ruff_python_semantic", @@ -1926,7 +1927,7 @@ dependencies = [ [[package]] name = "ruff_cli" -version = "0.0.275" +version = "0.0.276" dependencies = [ "annotate-snippets 0.9.1", "anyhow", diff --git a/README.md b/README.md index 46749a3e84..1a73e65b38 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.275 + rev: v0.0.276 hooks: - id: ruff ``` diff --git a/crates/flake8_to_ruff/Cargo.toml b/crates/flake8_to_ruff/Cargo.toml index f8191279e2..c411d33fe6 100644 --- a/crates/flake8_to_ruff/Cargo.toml +++ b/crates/flake8_to_ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flake8-to-ruff" -version = "0.0.275" +version = "0.0.276" description = """ Convert Flake8 configuration files to Ruff configuration files. """ diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 76b09f6474..c7a401b79c 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.0.275" +version = "0.0.276" publish = false authors = { workspace = true } edition = { workspace = true } @@ -17,6 +17,7 @@ name = "ruff" [dependencies] ruff_cache = { path = "../ruff_cache" } ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] } +ruff_index = { path = "../ruff_index" } ruff_macros = { path = "../ruff_macros" } ruff_python_whitespace = { path = "../ruff_python_whitespace" } ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] } @@ -88,3 +89,5 @@ colored = { workspace = true, features = ["no-color"] } [features] default = [] schemars = ["dep:schemars"] +# Enables the UnreachableCode rule +unreachable-code = [] diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/assert.py b/crates/ruff/resources/test/fixtures/control-flow-graph/assert.py new file mode 100644 index 0000000000..bfb3ab9030 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/assert.py @@ -0,0 +1,11 @@ +def func(): + assert True + +def func(): + assert False + +def func(): + assert True, "oops" + +def func(): + assert False, "oops" diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/async-for.py b/crates/ruff/resources/test/fixtures/control-flow-graph/async-for.py new file mode 100644 index 0000000000..a1dc86a6e9 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/async-for.py @@ -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 diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/for.py b/crates/ruff/resources/test/fixtures/control-flow-graph/for.py new file mode 100644 index 0000000000..a5807a635a --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/for.py @@ -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 diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/if.py b/crates/ruff/resources/test/fixtures/control-flow-graph/if.py new file mode 100644 index 0000000000..2b5fa42099 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/if.py @@ -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 diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/match.py b/crates/ruff/resources/test/fixtures/control-flow-graph/match.py new file mode 100644 index 0000000000..cce019e308 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/match.py @@ -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 :(") diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/raise.py b/crates/ruff/resources/test/fixtures/control-flow-graph/raise.py new file mode 100644 index 0000000000..37aadc61a0 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/raise.py @@ -0,0 +1,5 @@ +def func(): + raise Exception + +def func(): + raise "a glass!" diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/simple.py b/crates/ruff/resources/test/fixtures/control-flow-graph/simple.py new file mode 100644 index 0000000000..d1f710149b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/simple.py @@ -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 diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/try.py b/crates/ruff/resources/test/fixtures/control-flow-graph/try.py new file mode 100644 index 0000000000..e9f109dfd7 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/try.py @@ -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: + ... diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/while.py b/crates/ruff/resources/test/fixtures/control-flow-graph/while.py new file mode 100644 index 0000000000..6a4174358b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/while.py @@ -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) +''' diff --git a/crates/ruff/resources/test/fixtures/flake8_bandit/S307.py b/crates/ruff/resources/test/fixtures/flake8_bandit/S307.py new file mode 100644 index 0000000000..06bccc084a --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_bandit/S307.py @@ -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 diff --git a/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py index 43f1b986e9..917a848ba1 100644 --- a/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py +++ b/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py @@ -23,6 +23,10 @@ class Foobar(unittest.TestCase): with self.assertRaises(Exception): raise Exception("Evil I say!") + def also_evil_raises(self) -> None: + with self.assertRaises(BaseException): + raise Exception("Evil I say!") + def context_manager_raises(self) -> None: with self.assertRaises(Exception) as ex: raise Exception("Context manager is good") @@ -41,6 +45,9 @@ def test_pytest_raises(): with pytest.raises(Exception): raise ValueError("Hello") + with pytest.raises(Exception), pytest.raises(ValueError): + raise ValueError("Hello") + with pytest.raises(Exception, "hello"): raise ValueError("This is fine") diff --git a/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py b/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py index fc0ffd0cbb..a0f8d9da22 100644 --- a/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py +++ b/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py @@ -111,3 +111,19 @@ class PerfectlyFine(models.Model): @property def random_property(self): return "%s" % self + + +class MultipleConsecutiveFields(models.Model): + """Model that contains multiple out-of-order field definitions in a row.""" + + + class Meta: + verbose_name = "test" + + first_name = models.CharField(max_length=32) + last_name = models.CharField(max_length=32) + + def get_absolute_url(self): + pass + + middle_name = models.CharField(max_length=32) diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py new file mode 100644 index 0000000000..50cf7c884f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py @@ -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 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi new file mode 100644 index 0000000000..50cf7c884f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi @@ -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 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.py new file mode 100644 index 0000000000..9c4481179f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.py @@ -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 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.pyi new file mode 100644 index 0000000000..9c4481179f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.pyi @@ -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 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.py new file mode 100644 index 0000000000..bca3f9f7e3 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.py @@ -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': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.pyi new file mode 100644 index 0000000000..bca3f9f7e3 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.pyi @@ -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': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.py new file mode 100644 index 0000000000..0053e9e9df --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.py @@ -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': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.pyi new file mode 100644 index 0000000000..0053e9e9df --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.pyi @@ -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': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py index 37a4f4d867..16ca34e318 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py @@ -91,3 +91,4 @@ field27 = list[str] field28 = builtins.str field29 = str field30 = str | bytes | None +field31: typing.Final = field30 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi index 860ee255fb..10f7a70770 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi @@ -98,3 +98,4 @@ field27 = list[str] field28 = builtins.str field29 = str field30 = str | bytes | None +field31: typing.Final = field30 diff --git a/crates/ruff/resources/test/fixtures/numpy/NPY002.py b/crates/ruff/resources/test/fixtures/numpy/NPY002.py index d0e2274e6d..129b270cab 100644 --- a/crates/ruff/resources/test/fixtures/numpy/NPY002.py +++ b/crates/ruff/resources/test/fixtures/numpy/NPY002.py @@ -1,5 +1,6 @@ # Do this (new version) from numpy.random import default_rng + rng = default_rng() vals = rng.standard_normal(10) more_vals = rng.standard_normal(10) @@ -7,11 +8,13 @@ numbers = rng.integers(high, size=5) # instead of this (legacy version) from numpy import random + vals = random.standard_normal(10) more_vals = random.standard_normal(10) numbers = random.integers(high, size=5) import numpy + numpy.random.seed() numpy.random.get_state() numpy.random.set_state() diff --git a/crates/ruff/resources/test/fixtures/numpy/NPY003.py b/crates/ruff/resources/test/fixtures/numpy/NPY003.py new file mode 100644 index 0000000000..6d6f369771 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/numpy/NPY003.py @@ -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)) diff --git a/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py b/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py index 99dc33a327..4d1fc96b59 100644 --- a/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py +++ b/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py @@ -4,7 +4,9 @@ x = pd.DataFrame() x.drop(["a"], axis=1, inplace=True) -x.drop(["a"], axis=1, inplace=True) +x.y.drop(["a"], axis=1, inplace=True) + +x["y"].drop(["a"], axis=1, inplace=True) x.drop( inplace=True, @@ -23,6 +25,7 @@ x.drop(["a"], axis=1, **kwargs, inplace=True) x.drop(["a"], axis=1, inplace=True, **kwargs) f(x.drop(["a"], axis=1, inplace=True)) -x.apply(lambda x: x.sort_values('a', inplace=True)) +x.apply(lambda x: x.sort_values("a", inplace=True)) import torch -torch.m.ReLU(inplace=True) # safe because this isn't a pandas call + +torch.m.ReLU(inplace=True) # safe because this isn't a pandas call diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF401.py b/crates/ruff/resources/test/fixtures/perflint/PERF401.py new file mode 100644 index 0000000000..beb3d4546c --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF401.py @@ -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 diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF402.py b/crates/ruff/resources/test/fixtures/perflint/PERF402.py new file mode 100644 index 0000000000..4db9a3dc52 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF402.py @@ -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 diff --git a/crates/ruff/resources/test/fixtures/pydocstyle/D410.py b/crates/ruff/resources/test/fixtures/pydocstyle/D410.py new file mode 100644 index 0000000000..b9aec80568 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pydocstyle/D410.py @@ -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 + ------- + """ diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py index 266e8431cc..879f3842ad 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py @@ -70,3 +70,8 @@ print("foo".encode()) # print(b"foo") "abc" "def" )).encode() + +(f"foo{bar}").encode("utf-8") +(f"foo{bar}").encode(encoding="utf-8") +("unicode text©").encode("utf-8") +("unicode text©").encode(encoding="utf-8") diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF014.py b/crates/ruff/resources/test/fixtures/ruff/RUF014.py new file mode 100644 index 0000000000..d1ae40f3ca --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF014.py @@ -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) +''' diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 012d999ea3..67752031eb 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -36,6 +36,7 @@ use crate::importer::Importer; use crate::noqa::NoqaMapping; use crate::registry::Rule; use crate::rules::flake8_builtins::helpers::AnyShadowing; + use crate::rules::{ airflow, flake8_2020, flake8_annotations, flake8_async, flake8_bandit, flake8_blind_except, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, @@ -71,7 +72,7 @@ pub(crate) struct Checker<'a> { deferred: Deferred<'a>, pub(crate) diagnostics: Vec, // Check-specific state. - pub(crate) flake8_bugbear_seen: Vec<&'a Expr>, + pub(crate) flake8_bugbear_seen: Vec<&'a ast::ExprName>, } impl<'a> Checker<'a> { @@ -358,11 +359,7 @@ where .. }) => { if self.enabled(Rule::DjangoNonLeadingReceiverDecorator) { - self.diagnostics - .extend(flake8_django::rules::non_leading_receiver_decorator( - decorator_list, - |expr| self.semantic.resolve_call_path(expr), - )); + flake8_django::rules::non_leading_receiver_decorator(self, decorator_list); } if self.enabled(Rule::AmbiguousFunctionName) { if let Some(diagnostic) = @@ -504,8 +501,7 @@ where } } if self.enabled(Rule::HardcodedPasswordDefault) { - self.diagnostics - .extend(flake8_bandit::rules::hardcoded_password_default(args)); + flake8_bandit::rules::hardcoded_password_default(self, args); } if self.enabled(Rule::PropertyWithParameters) { pylint::rules::property_with_parameters(self, stmt, decorator_list, args); @@ -623,6 +619,11 @@ where ); } } + #[cfg(feature = "unreachable-code")] + if self.enabled(Rule::UnreachableCode) { + self.diagnostics + .extend(ruff::rules::unreachable::in_function(name, body)); + } } Stmt::Return(_) => { if self.enabled(Rule::ReturnOutsideFunction) { @@ -643,10 +644,7 @@ where }, ) => { if self.enabled(Rule::DjangoNullableModelStringField) { - self.diagnostics - .extend(flake8_django::rules::nullable_model_string_field( - self, body, - )); + flake8_django::rules::nullable_model_string_field(self, body); } if self.enabled(Rule::DjangoExcludeWithModelForm) { if let Some(diagnostic) = @@ -667,21 +665,17 @@ where } if !self.is_stub { if self.enabled(Rule::DjangoModelWithoutDunderStr) { - if let Some(diagnostic) = - flake8_django::rules::model_without_dunder_str(self, bases, body, stmt) - { - self.diagnostics.push(diagnostic); - } + flake8_django::rules::model_without_dunder_str(self, class_def); } } if self.enabled(Rule::GlobalStatement) { pylint::rules::global_statement(self, name); } if self.enabled(Rule::UselessObjectInheritance) { - pyupgrade::rules::useless_object_inheritance(self, class_def, stmt); + pyupgrade::rules::useless_object_inheritance(self, class_def); } if self.enabled(Rule::UnnecessaryClassParentheses) { - pyupgrade::rules::unnecessary_class_parentheses(self, class_def, stmt); + pyupgrade::rules::unnecessary_class_parentheses(self, class_def); } if self.enabled(Rule::AmbiguousClassName) { if let Some(diagnostic) = @@ -1370,6 +1364,51 @@ where self.diagnostics.push(diagnostic); } } + if self.is_stub { + if self.any_enabled(&[ + Rule::UnrecognizedVersionInfoCheck, + Rule::PatchVersionComparison, + Rule::WrongTupleLengthVersionComparison, + ]) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::unrecognized_version_info(self, value); + } + } else { + flake8_pyi::rules::unrecognized_version_info(self, test); + } + } + if self.any_enabled(&[ + Rule::UnrecognizedPlatformCheck, + Rule::UnrecognizedPlatformName, + ]) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::unrecognized_platform(self, value); + } + } else { + flake8_pyi::rules::unrecognized_platform(self, test); + } + } + if self.enabled(Rule::BadVersionInfoComparison) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::bad_version_info_comparison(self, value); + } + } else { + flake8_pyi::rules::bad_version_info_comparison(self, test); + } + } + if self.enabled(Rule::ComplexIfStatementInStub) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::complex_if_statement_in_stub(self, value); + } + } else { + flake8_pyi::rules::complex_if_statement_in_stub(self, test); + } + } + } } Stmt::Assert(ast::StmtAssert { test, @@ -1409,7 +1448,7 @@ where Stmt::With(ast::StmtWith { items, body, .. }) | Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => { if self.enabled(Rule::AssertRaisesException) { - flake8_bugbear::rules::assert_raises_exception(self, stmt, items); + flake8_bugbear::rules::assert_raises_exception(self, items); } if self.enabled(Rule::PytestRaisesWithMultipleStatements) { flake8_pytest_style::rules::complex_raises(self, stmt, items, body); @@ -1490,6 +1529,12 @@ where if self.enabled(Rule::IncorrectDictIterator) { perflint::rules::incorrect_dict_iterator(self, target, iter); } + if self.enabled(Rule::ManualListComprehension) { + perflint::rules::manual_list_comprehension(self, target, body); + } + if self.enabled(Rule::ManualListCopy) { + perflint::rules::manual_list_copy(self, target, body); + } if self.enabled(Rule::UnnecessaryListCast) { perflint::rules::unnecessary_list_cast(self, iter); } @@ -1528,9 +1573,7 @@ where pyupgrade::rules::os_error_alias_handlers(self, handlers); } if self.enabled(Rule::PytestAssertInExcept) { - self.diagnostics.extend( - flake8_pytest_style::rules::assert_in_exception_handler(handlers), - ); + flake8_pytest_style::rules::assert_in_exception_handler(self, handlers); } if self.enabled(Rule::SuppressibleException) { flake8_simplify::rules::suppressible_exception( @@ -1571,11 +1614,7 @@ where flake8_bugbear::rules::assignment_to_os_environ(self, targets); } if self.enabled(Rule::HardcodedPasswordString) { - if let Some(diagnostic) = - flake8_bandit::rules::assign_hardcoded_password_string(value, targets) - { - self.diagnostics.push(diagnostic); - } + flake8_bandit::rules::assign_hardcoded_password_string(self, value, targets); } if self.enabled(Rule::GlobalStatement) { for target in targets.iter() { @@ -2107,6 +2146,7 @@ where && self.settings.target_version >= PythonVersion::Py37 && !self.semantic.future_annotations() && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( self, value, @@ -2117,7 +2157,8 @@ where if self.settings.target_version >= PythonVersion::Py310 || (self.settings.target_version >= PythonVersion::Py37 && self.semantic.future_annotations() - && self.semantic.in_annotation()) + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep604_annotation( self, expr, slice, operator, @@ -2193,6 +2234,9 @@ where if self.enabled(Rule::NumpyDeprecatedTypeAlias) { numpy::rules::deprecated_type_alias(self, expr); } + if self.enabled(Rule::NumpyDeprecatedFunction) { + numpy::rules::deprecated_function(self, expr); + } if self.is_stub { if self.enabled(Rule::CollectionsNamedTuple) { flake8_pyi::rules::collections_named_tuple(self, expr); @@ -2212,6 +2256,7 @@ where && self.settings.target_version >= PythonVersion::Py37 && !self.semantic.future_annotations() && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( self, expr, @@ -2222,7 +2267,8 @@ where if self.settings.target_version >= PythonVersion::Py39 || (self.settings.target_version >= PythonVersion::Py37 && self.semantic.future_annotations() - && self.semantic.in_annotation()) + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep585_annotation( self, @@ -2287,6 +2333,7 @@ where && self.settings.target_version >= PythonVersion::Py37 && !self.semantic.future_annotations() && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( self, expr, @@ -2297,7 +2344,8 @@ where if self.settings.target_version >= PythonVersion::Py39 || (self.settings.target_version >= PythonVersion::Py37 && self.semantic.future_annotations() - && self.semantic.in_annotation()) + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep585_annotation(self, expr, &replacement); } @@ -2315,6 +2363,9 @@ where if self.enabled(Rule::NumpyDeprecatedTypeAlias) { numpy::rules::deprecated_type_alias(self, expr); } + if self.enabled(Rule::NumpyDeprecatedFunction) { + numpy::rules::deprecated_function(self, expr); + } if self.enabled(Rule::DeprecatedMockImport) { pyupgrade::rules::deprecated_mock_attribute(self, expr); } @@ -2533,9 +2584,7 @@ where flake8_pie::rules::unnecessary_dict_kwargs(self, expr, keywords); } if self.enabled(Rule::ExecBuiltin) { - if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) { - self.diagnostics.push(diagnostic); - } + flake8_bandit::rules::exec_used(self, func); } if self.enabled(Rule::BadFilePermissions) { flake8_bandit::rules::bad_file_permissions(self, func, args, keywords); @@ -2558,8 +2607,7 @@ where flake8_bandit::rules::jinja2_autoescape_false(self, func, args, keywords); } if self.enabled(Rule::HardcodedPasswordFuncArg) { - self.diagnostics - .extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords)); + flake8_bandit::rules::hardcoded_password_func_arg(self, keywords); } if self.enabled(Rule::HardcodedSQLExpression) { flake8_bandit::rules::hardcoded_sql_expression(self, expr); @@ -2692,17 +2740,12 @@ where flake8_debugger::rules::debugger_call(self, expr, func); } if self.enabled(Rule::PandasUseOfInplaceArgument) { - self.diagnostics.extend( - pandas_vet::rules::inplace_argument(self, expr, func, args, keywords) - .into_iter(), - ); + pandas_vet::rules::inplace_argument(self, expr, func, args, keywords); } pandas_vet::rules::call(self, func); if self.enabled(Rule::PandasUseOfPdMerge) { - if let Some(diagnostic) = pandas_vet::rules::use_of_pd_merge(func) { - self.diagnostics.push(diagnostic); - }; + pandas_vet::rules::use_of_pd_merge(self, func); } if self.enabled(Rule::CallDatetimeWithoutTzinfo) { flake8_datetimez::rules::call_datetime_without_tzinfo( @@ -2819,16 +2862,13 @@ where &self.settings.flake8_gettext.functions_names, ) { if self.enabled(Rule::FStringInGetTextFuncCall) { - self.diagnostics - .extend(flake8_gettext::rules::f_string_in_gettext_func_call(args)); + flake8_gettext::rules::f_string_in_gettext_func_call(self, args); } if self.enabled(Rule::FormatInGetTextFuncCall) { - self.diagnostics - .extend(flake8_gettext::rules::format_in_gettext_func_call(args)); + flake8_gettext::rules::format_in_gettext_func_call(self, args); } if self.enabled(Rule::PrintfInGetTextFuncCall) { - self.diagnostics - .extend(flake8_gettext::rules::printf_in_gettext_func_call(args)); + flake8_gettext::rules::printf_in_gettext_func_call(self, args); } } if self.enabled(Rule::UncapitalizedEnvironmentVariables) { @@ -2869,7 +2909,7 @@ where flake8_use_pathlib::rules::replaceable_by_pathlib(self, func); } if self.enabled(Rule::NumpyLegacyRandom) { - numpy::rules::numpy_legacy_random(self, func); + numpy::rules::legacy_random(self, func); } if self.any_enabled(&[ Rule::LoggingStringFormat, @@ -3169,11 +3209,10 @@ where flake8_2020::rules::compare(self, left, ops, comparators); } if self.enabled(Rule::HardcodedPasswordString) { - self.diagnostics.extend( - flake8_bandit::rules::compare_to_hardcoded_password_string( - left, - comparators, - ), + flake8_bandit::rules::compare_to_hardcoded_password_string( + self, + left, + comparators, ); } if self.enabled(Rule::ComparisonWithItself) { @@ -3194,29 +3233,6 @@ where if self.enabled(Rule::YodaConditions) { flake8_simplify::rules::yoda_conditions(self, expr, left, ops, comparators); } - if self.is_stub { - if self.any_enabled(&[ - Rule::UnrecognizedPlatformCheck, - Rule::UnrecognizedPlatformName, - ]) { - flake8_pyi::rules::unrecognized_platform( - self, - expr, - left, - ops, - comparators, - ); - } - if self.enabled(Rule::BadVersionInfoComparison) { - flake8_pyi::rules::bad_version_info_comparison( - self, - expr, - left, - ops, - comparators, - ); - } - } } Expr::Constant(ast::ExprConstant { value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. }, @@ -4422,7 +4438,7 @@ impl<'a> Checker<'a> { } fn handle_node_delete(&mut self, expr: &'a Expr) { - let Expr::Name(ast::ExprName { id, .. } )= expr else { + let Expr::Name(ast::ExprName { id, .. }) = expr else { return; }; diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 15b334e884..5e526f599d 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -596,6 +596,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // flake8-pyi (Flake8Pyi, "001") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnprefixedTypeParam), + (Flake8Pyi, "002") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ComplexIfStatementInStub), + (Flake8Pyi, "003") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedVersionInfoCheck), + (Flake8Pyi, "004") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::PatchVersionComparison), + (Flake8Pyi, "005") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::WrongTupleLengthVersionComparison), (Flake8Pyi, "006") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadVersionInfoComparison), (Flake8Pyi, "007") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformCheck), (Flake8Pyi, "008") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformName), @@ -742,6 +746,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // numpy (Numpy, "001") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedTypeAlias), (Numpy, "002") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyLegacyRandom), + (Numpy, "003") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedFunction), // ruff (Ruff, "001") => (RuleGroup::Unspecified, rules::ruff::rules::AmbiguousUnicodeCharacterString), @@ -756,6 +761,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "011") => (RuleGroup::Unspecified, rules::ruff::rules::StaticKeyDictComprehension), (Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault), (Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional), + #[cfg(feature = "unreachable-code")] + (Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode), (Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA), (Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml), @@ -788,6 +795,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Perflint, "101") => (RuleGroup::Unspecified, rules::perflint::rules::UnnecessaryListCast), (Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator), (Perflint, "203") => (RuleGroup::Unspecified, rules::perflint::rules::TryExceptInLoop), + (Perflint, "401") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListComprehension), + (Perflint, "402") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListCopy), // flake8-fixme (Flake8Fixme, "001") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsFixme), diff --git a/crates/ruff/src/docstrings/sections.rs b/crates/ruff/src/docstrings/sections.rs index 48f38e606d..6bdca985f4 100644 --- a/crates/ruff/src/docstrings/sections.rs +++ b/crates/ruff/src/docstrings/sections.rs @@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words}; use ruff_text_size::{TextLen, TextRange, TextSize}; use strum_macros::EnumIter; -use ruff_python_whitespace::{UniversalNewlineIterator, UniversalNewlines}; +use ruff_python_whitespace::{Line, UniversalNewlineIterator, UniversalNewlines}; use crate::docstrings::styles::SectionStyle; use crate::docstrings::{Docstring, DocstringBody}; @@ -144,15 +144,13 @@ impl<'a> SectionContexts<'a> { let mut contexts = Vec::new(); let mut last: Option = None; - let mut previous_line = None; - for line in contents.universal_newlines() { - if previous_line.is_none() { - // skip the first line - previous_line = Some(line.as_str()); - continue; - } + let mut lines = contents.universal_newlines().peekable(); + // Skip the first line, which is the summary. + let mut previous_line = lines.next(); + + while let Some(line) = lines.next() { if let Some(section_kind) = suspected_as_section(&line, style) { let indent = leading_space(&line); let section_name = leading_words(&line); @@ -162,7 +160,8 @@ impl<'a> SectionContexts<'a> { if is_docstring_section( &line, section_name_range, - previous_line.unwrap_or_default(), + previous_line.as_ref(), + lines.peek(), ) { if let Some(mut last) = last.take() { last.range = TextRange::new(last.range.start(), line.start()); @@ -178,7 +177,7 @@ impl<'a> SectionContexts<'a> { } } - previous_line = Some(line.as_str()); + previous_line = Some(line); } if let Some(mut last) = last.take() { @@ -388,7 +387,13 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option } /// Check if the suspected context is really a section header. -fn is_docstring_section(line: &str, section_name_range: TextRange, previous_lines: &str) -> bool { +fn is_docstring_section( + line: &Line, + section_name_range: TextRange, + previous_line: Option<&Line>, + next_line: Option<&Line>, +) -> bool { + // Determine whether the current line looks like a section header, e.g., "Args:". let section_name_suffix = line[usize::from(section_name_range.end())..].trim(); let this_looks_like_a_section_name = section_name_suffix == ":" || section_name_suffix.is_empty(); @@ -396,13 +401,29 @@ fn is_docstring_section(line: &str, section_name_range: TextRange, previous_line return false; } - let prev_line = previous_lines.trim(); - let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')'] - .into_iter() - .any(|char| prev_line.ends_with(char)); - let prev_line_looks_like_end_of_paragraph = - prev_line_ends_with_punctuation || prev_line.is_empty(); - if !prev_line_looks_like_end_of_paragraph { + // Determine whether the next line is an underline, e.g., "-----". + let next_line_is_underline = next_line.map_or(false, |next_line| { + let next_line = next_line.trim(); + if next_line.is_empty() { + false + } else { + let next_line_is_underline = next_line.chars().all(|char| matches!(char, '-' | '=')); + next_line_is_underline + } + }); + if next_line_is_underline { + return true; + } + + // Determine whether the previous line looks like the end of a paragraph. + let previous_line_looks_like_end_of_paragraph = previous_line.map_or(true, |previous_line| { + let previous_line = previous_line.trim(); + let previous_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')'] + .into_iter() + .any(|char| previous_line.ends_with(char)); + previous_line_ends_with_punctuation || previous_line.is_empty() + }); + if !previous_line_looks_like_end_of_paragraph { return false; } diff --git a/crates/ruff/src/flake8_to_ruff/plugin.rs b/crates/ruff/src/flake8_to_ruff/plugin.rs index 77f6645d29..c234556c8a 100644 --- a/crates/ruff/src/flake8_to_ruff/plugin.rs +++ b/crates/ruff/src/flake8_to_ruff/plugin.rs @@ -333,7 +333,7 @@ pub(crate) fn infer_plugins_from_codes(selectors: &HashSet) -> Vec for selector in selectors { if selector .into_iter() - .any(|rule| Linter::from(plugin).into_iter().any(|r| r == rule)) + .any(|rule| Linter::from(plugin).rules().any(|r| r == rule)) { return true; } diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index 91aa62e24d..3c3c0154b3 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -268,11 +268,12 @@ impl Notebook { .markers() .iter() .rev() - .find(|m| m.source <= *offset) else { - // There are no markers above the current offset, so we can - // stop here. - break; - }; + .find(|m| m.source <= *offset) + else { + // There are no markers above the current offset, so we can + // stop here. + break; + }; last_marker = Some(marker); marker } diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index dd47f24815..d53cd6ac7e 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -19,7 +19,7 @@ impl Rule { pub fn from_code(code: &str) -> Result { let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?; let prefix: RuleCodePrefix = RuleCodePrefix::parse(&linter, code)?; - Ok(prefix.into_iter().next().unwrap()) + Ok(prefix.rules().next().unwrap()) } } diff --git a/crates/ruff/src/registry/rule_set.rs b/crates/ruff/src/registry/rule_set.rs index 97e7ac7f3b..555ba0e5b2 100644 --- a/crates/ruff/src/registry/rule_set.rs +++ b/crates/ruff/src/registry/rule_set.rs @@ -3,7 +3,7 @@ use ruff_macros::CacheKey; use std::fmt::{Debug, Formatter}; use std::iter::FusedIterator; -const RULESET_SIZE: usize = 10; +const RULESET_SIZE: usize = 11; /// A set of [`Rule`]s. /// diff --git a/crates/ruff/src/rule_selector.rs b/crates/ruff/src/rule_selector.rs index 6247346a51..5eb5f1461b 100644 --- a/crates/ruff/src/rule_selector.rs +++ b/crates/ruff/src/rule_selector.rs @@ -158,16 +158,16 @@ impl IntoIterator for &RuleSelector { } RuleSelector::C => RuleSelectorIter::Chain( Linter::Flake8Comprehensions - .into_iter() - .chain(Linter::McCabe.into_iter()), + .rules() + .chain(Linter::McCabe.rules()), ), RuleSelector::T => RuleSelectorIter::Chain( Linter::Flake8Debugger - .into_iter() - .chain(Linter::Flake8Print.into_iter()), + .rules() + .chain(Linter::Flake8Print.rules()), ), - RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.into_iter()), - RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.into_iter()), + RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.rules()), + RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.clone().rules()), } } } @@ -346,7 +346,7 @@ mod clap_completion { let prefix = p.linter().common_prefix(); let code = p.short_code(); - let mut rules_iter = p.into_iter(); + let mut rules_iter = p.rules(); let rule1 = rules_iter.next(); let rule2 = rules_iter.next(); diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index 5fdb814dab..47232b9e04 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -457,11 +457,7 @@ pub(crate) fn definition( // TODO(charlie): Consider using the AST directly here rather than `Definition`. // We could adhere more closely to `flake8-annotations` by defining public // vs. secret vs. protected. - let Definition::Member(Member { - kind, - stmt, - .. - }) = definition else { + let Definition::Member(Member { kind, stmt, .. }) = definition else { return vec![]; }; diff --git a/crates/ruff/src/rules/flake8_bandit/mod.rs b/crates/ruff/src/rules/flake8_bandit/mod.rs index 4abd69f58a..87d0e449eb 100644 --- a/crates/ruff/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff/src/rules/flake8_bandit/mod.rs @@ -39,6 +39,7 @@ mod tests { #[test_case(Rule::SubprocessPopenWithShellEqualsTrue, Path::new("S602.py"))] #[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))] #[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"))] + #[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))] #[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))] #[test_case(Rule::TryExceptContinue, Path::new("S112.py"))] #[test_case(Rule::TryExceptPass, Path::new("S110.py"))] diff --git a/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs b/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs index 3ff3db8ded..d2dfb83fb5 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs @@ -1,8 +1,10 @@ -use rustpython_parser::ast::{self, Expr, Ranged}; +use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + #[violation] pub struct ExecBuiltin; @@ -14,12 +16,16 @@ impl Violation for ExecBuiltin { } /// S102 -pub(crate) fn exec_used(expr: &Expr, func: &Expr) -> Option { - let Expr::Name(ast::ExprName { id, .. }) = func else { - return None; - }; - if id != "exec" { - return None; +pub(crate) fn exec_used(checker: &mut Checker, func: &Expr) { + if checker + .semantic() + .resolve_call_path(func) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["" | "builtin", "exec"]) + }) + { + checker + .diagnostics + .push(Diagnostic::new(ExecBuiltin, func.range())); } - Some(Diagnostic::new(ExecBuiltin, expr.range())) } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs index 127ce31199..50be800d9e 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs @@ -1,5 +1,6 @@ use rustpython_parser::ast::{Arg, ArgWithDefault, Arguments, Expr, Ranged}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -36,9 +37,7 @@ fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option { } /// S107 -pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec { - let mut diagnostics: Vec = Vec::new(); - +pub(crate) fn hardcoded_password_default(checker: &mut Checker, arguments: &Arguments) { for ArgWithDefault { def, default, @@ -53,9 +52,7 @@ pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec Vec { - keywords - .iter() - .filter_map(|keyword| { +pub(crate) fn hardcoded_password_func_arg(checker: &mut Checker, keywords: &[Keyword]) { + checker + .diagnostics + .extend(keywords.iter().filter_map(|keyword| { string_literal(&keyword.value).filter(|string| !string.is_empty())?; let arg = keyword.arg.as_ref()?; if !matches_password_name(arg) { @@ -37,6 +38,5 @@ pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec Option<&str> { /// S105 pub(crate) fn compare_to_hardcoded_password_string( + checker: &mut Checker, left: &Expr, comparators: &[Expr], -) -> Vec { - comparators - .iter() - .filter_map(|comp| { +) { + checker + .diagnostics + .extend(comparators.iter().filter_map(|comp| { string_literal(comp).filter(|string| !string.is_empty())?; let Some(name) = password_target(left) else { return None; @@ -63,29 +66,29 @@ pub(crate) fn compare_to_hardcoded_password_string( }, comp.range(), )) - }) - .collect() + })); } /// S105 pub(crate) fn assign_hardcoded_password_string( + checker: &mut Checker, value: &Expr, targets: &[Expr], -) -> Option { +) { if string_literal(value) .filter(|string| !string.is_empty()) .is_some() { for target in targets { if let Some(name) = password_target(target) { - return Some(Diagnostic::new( + checker.diagnostics.push(Diagnostic::new( HardcodedPasswordString { name: name.to_string(), }, value.range(), )); + return; } } } - None } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs index 41e426b34d..51b200b67e 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs @@ -67,7 +67,7 @@ fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Optio return None; }; // Only evaluate the full BinOp, not the nested components. - let Expr::BinOp(_ )= parent else { + let Expr::BinOp(_) = parent else { if any_over_expr(expr, &has_string_literal) { return Some(checker.generator().expr(expr)); } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs b/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs index b03020e7eb..27c22af441 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs @@ -21,21 +21,6 @@ impl Violation for RequestWithNoCertValidation { } } -const REQUESTS_HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"]; -const HTTPX_METHODS: [&str; 11] = [ - "get", - "options", - "head", - "post", - "put", - "patch", - "delete", - "request", - "stream", - "Client", - "AsyncClient", -]; - /// S501 pub(crate) fn request_with_no_cert_validation( checker: &mut Checker, @@ -46,16 +31,13 @@ pub(crate) fn request_with_no_cert_validation( if let Some(target) = checker .semantic() .resolve_call_path(func) - .and_then(|call_path| { - if call_path.len() == 2 { - if call_path[0] == "requests" && REQUESTS_HTTP_VERBS.contains(&call_path[1]) { - return Some("requests"); - } - if call_path[0] == "httpx" && HTTPX_METHODS.contains(&call_path[1]) { - return Some("httpx"); - } + .and_then(|call_path| match call_path.as_slice() { + ["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete"] => { + Some("requests") } - None + ["httpx", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request" + | "stream" | "Client" | "AsyncClient"] => Some("httpx"), + _ => None, }) { let call_args = SimpleCallArgs::new(args, keywords); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs b/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs index c164dcadc1..08736d5fc9 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs @@ -1,31 +1,28 @@ -use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; +use rustpython_parser::ast::{Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::SimpleCallArgs; +use ruff_python_ast::helpers::{is_const_none, SimpleCallArgs}; use crate::checkers::ast::Checker; #[violation] pub struct RequestWithoutTimeout { - pub timeout: Option, + implicit: bool, } impl Violation for RequestWithoutTimeout { #[derive_message_formats] fn message(&self) -> String { - let RequestWithoutTimeout { timeout } = self; - match timeout { - Some(value) => { - format!("Probable use of requests call with timeout set to `{value}`") - } - None => format!("Probable use of requests call without timeout"), + let RequestWithoutTimeout { implicit } = self; + if *implicit { + format!("Probable use of requests call without timeout") + } else { + format!("Probable use of requests call with timeout set to `None`") } } } -const HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"]; - /// S113 pub(crate) fn request_without_timeout( checker: &mut Checker, @@ -37,30 +34,26 @@ pub(crate) fn request_without_timeout( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - HTTP_VERBS - .iter() - .any(|func_name| call_path.as_slice() == ["requests", func_name]) + matches!( + call_path.as_slice(), + [ + "requests", + "get" | "options" | "head" | "post" | "put" | "patch" | "delete" + ] + ) }) { let call_args = SimpleCallArgs::new(args, keywords); - if let Some(timeout_arg) = call_args.keyword_argument("timeout") { - if let Some(timeout) = match timeout_arg { - Expr::Constant(ast::ExprConstant { - value: value @ Constant::None, - .. - }) => Some(checker.generator().constant(value)), - _ => None, - } { + if let Some(timeout) = call_args.keyword_argument("timeout") { + if is_const_none(timeout) { checker.diagnostics.push(Diagnostic::new( - RequestWithoutTimeout { - timeout: Some(timeout), - }, - timeout_arg.range(), + RequestWithoutTimeout { implicit: false }, + timeout.range(), )); } } else { checker.diagnostics.push(Diagnostic::new( - RequestWithoutTimeout { timeout: None }, + RequestWithoutTimeout { implicit: true }, func.range(), )); } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs index cfbb0df50c..6a2add760d 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -219,7 +219,7 @@ impl Violation for SuspiciousFTPLibUsage { } } -/// S001 +/// S301, S302, S303, S304, S305, S306, S307, S308, S310, S311, S312, S313, S314, S315, S316, S317, S318, S319, S320, S321, S323 pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) { let Expr::Call(ast::ExprCall { func, .. }) = expr else { return; @@ -246,7 +246,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) { // Mktemp ["tempfile", "mktemp"] => Some(SuspiciousMktempUsage.into()), // Eval - ["eval"] => Some(SuspiciousEvalUsage.into()), + ["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()), // MarkSafe ["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()), // URLOpen diff --git a/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap index ccf9572377..075092ceda 100644 --- a/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap +++ b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap @@ -6,7 +6,7 @@ S102.py:3:5: S102 Use of `exec` detected 1 | def fn(): 2 | # Error 3 | exec('x = 2') - | ^^^^^^^^^^^^^ S102 + | ^^^^ S102 4 | 5 | exec('y = 3') | @@ -16,7 +16,7 @@ S102.py:5:1: S102 Use of `exec` detected 3 | exec('x = 2') 4 | 5 | exec('y = 3') - | ^^^^^^^^^^^^^ S102 + | ^^^^ S102 | diff --git a/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S307_S307.py.snap b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S307_S307.py.snap new file mode 100644 index 0000000000..f5c6ac82d8 --- /dev/null +++ b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S307_S307.py.snap @@ -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 + | + + diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs b/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs index 796bcd17df..4c16954baa 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs @@ -161,19 +161,19 @@ pub(crate) fn abstract_base_class( continue; } - let ( - Stmt::FunctionDef(ast::StmtFunctionDef { - decorator_list, - body, - name: method_name, - .. - }) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - decorator_list, - body, - name: method_name, - .. - }) - ) = stmt else { + let (Stmt::FunctionDef(ast::StmtFunctionDef { + decorator_list, + body, + name: method_name, + .. + }) + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { + decorator_list, + body, + name: method_name, + .. + })) = stmt + else { continue; }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs index 7dfb6c6d91..00c44bf0ee 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs @@ -1,22 +1,20 @@ -use rustpython_parser::ast::{self, Expr, Ranged, Stmt, WithItem}; +use std::fmt; + +use rustpython_parser::ast::{self, Expr, Ranged, WithItem}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; -#[derive(Debug, PartialEq, Eq)] -pub(crate) enum AssertionKind { - AssertRaises, - PytestRaises, -} - /// ## What it does -/// Checks for `self.assertRaises(Exception)` or `pytest.raises(Exception)`. +/// Checks for `assertRaises` and `pytest.raises` context managers that catch +/// `Exception` or `BaseException`. /// /// ## Why is this bad? /// These forms catch every `Exception`, which can lead to tests passing even -/// if, e.g., the code being tested is never executed due to a typo. +/// if, e.g., the code under consideration raises a `SyntaxError` or +/// `IndentationError`. /// /// Either assert for a more specific exception (builtin or custom), or use /// `assertRaisesRegex` or `pytest.raises(..., match=)` respectively. @@ -32,51 +30,83 @@ pub(crate) enum AssertionKind { /// ``` #[violation] pub struct AssertRaisesException { - kind: AssertionKind, + assertion: AssertionKind, + exception: ExceptionKind, } impl Violation for AssertRaisesException { #[derive_message_formats] fn message(&self) -> String { - match self.kind { - AssertionKind::AssertRaises => { - format!("`assertRaises(Exception)` should be considered evil") - } - AssertionKind::PytestRaises => { - format!("`pytest.raises(Exception)` should be considered evil") - } + let AssertRaisesException { + assertion, + exception, + } = self; + format!("`{assertion}({exception})` should be considered evil") + } +} + +#[derive(Debug, PartialEq, Eq)] +enum AssertionKind { + AssertRaises, + PytestRaises, +} + +impl fmt::Display for AssertionKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + AssertionKind::AssertRaises => fmt.write_str("assertRaises"), + AssertionKind::PytestRaises => fmt.write_str("pytest.raises"), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum ExceptionKind { + BaseException, + Exception, +} + +impl fmt::Display for ExceptionKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + ExceptionKind::BaseException => fmt.write_str("BaseException"), + ExceptionKind::Exception => fmt.write_str("Exception"), } } } /// B017 -pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[WithItem]) { - let Some(item) = items.first() else { - return; - }; - let item_context = &item.context_expr; - let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &item_context else { - return; - }; - if args.len() != 1 { - return; - } - if item.optional_vars.is_some() { - return; - } +pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) { + for item in items { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + range: _, + }) = &item.context_expr + else { + return; + }; + if args.len() != 1 { + return; + } + if item.optional_vars.is_some() { + return; + } - if !checker - .semantic() - .resolve_call_path(args.first().unwrap()) - .map_or(false, |call_path| { - matches!(call_path.as_slice(), ["", "Exception"]) - }) - { - return; - } + let Some(exception) = checker + .semantic() + .resolve_call_path(args.first().unwrap()) + .and_then(|call_path| match call_path.as_slice() { + ["", "Exception"] => Some(ExceptionKind::Exception), + ["", "BaseException"] => Some(ExceptionKind::BaseException), + _ => None, + }) + else { + return; + }; - let kind = { - if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises") + let assertion = if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises") { AssertionKind::AssertRaises } else if checker @@ -92,11 +122,14 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: AssertionKind::PytestRaises } else { return; - } - }; + }; - checker.diagnostics.push(Diagnostic::new( - AssertRaisesException { kind }, - stmt.range(), - )); + checker.diagnostics.push(Diagnostic::new( + AssertRaisesException { + assertion, + exception, + }, + item.range(), + )); + } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs index dbd9ad3d0e..50f3f32922 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs @@ -59,7 +59,7 @@ pub(crate) fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr]) if attr != "environ" { return; } - let Expr::Name(ast::ExprName { id, .. } )= value.as_ref() else { + let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else { return; }; if id != "os" { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs index 1e8f8edb18..388d4a0128 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -166,7 +166,11 @@ pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHand let mut seen: FxHashSet = FxHashSet::default(); let mut duplicates: FxHashMap> = FxHashMap::default(); for handler in handlers { - let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_: Some(type_), .. }) = handler else { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_: Some(type_), + .. + }) = handler + else { continue; }; match type_.as_ref() { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs index ab362356af..cd195a8635 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs @@ -47,7 +47,7 @@ impl Violation for ExceptWithNonExceptionClasses { /// This should leave any unstarred iterables alone (subsequently raising a /// warning for B029). fn flatten_starred_iterables(expr: &Expr) -> Vec<&Expr> { - let Expr::Tuple(ast::ExprTuple { elts, .. } )= expr else { + let Expr::Tuple(ast::ExprTuple { elts, .. }) = expr else { return vec![expr]; }; let mut flattened_exprs: Vec<&Expr> = Vec::with_capacity(elts.len()); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs index 9741b37c7e..edba6aa901 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs @@ -1,9 +1,8 @@ -use rustc_hash::FxHashSet; use rustpython_parser::ast::{self, Comprehension, Expr, ExprContext, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::collect_arg_names; +use ruff_python_ast::helpers::includes_arg_name; use ruff_python_ast::types::Node; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -58,19 +57,17 @@ impl Violation for FunctionUsesLoopVariable { #[derive(Default)] struct LoadedNamesVisitor<'a> { - // Tuple of: name, defining expression, and defining range. - loaded: Vec<(&'a str, &'a Expr)>, - // Tuple of: name, defining expression, and defining range. - stored: Vec<(&'a str, &'a Expr)>, + loaded: Vec<&'a ast::ExprName>, + stored: Vec<&'a ast::ExprName>, } /// `Visitor` to collect all used identifiers in a statement. impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match expr { - Expr::Name(ast::ExprName { id, ctx, range: _ }) => match ctx { - ExprContext::Load => self.loaded.push((id, expr)), - ExprContext::Store => self.stored.push((id, expr)), + Expr::Name(name) => match &name.ctx { + ExprContext::Load => self.loaded.push(name), + ExprContext::Store => self.stored.push(name), ExprContext::Del => {} }, _ => visitor::walk_expr(self, expr), @@ -80,7 +77,7 @@ impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> { #[derive(Default)] struct SuspiciousVariablesVisitor<'a> { - names: Vec<(&'a str, &'a Expr)>, + names: Vec<&'a ast::ExprName>, safe_functions: Vec<&'a Expr>, } @@ -95,17 +92,20 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { let mut visitor = LoadedNamesVisitor::default(); visitor.visit_body(body); - // Collect all argument names. - let mut arg_names = collect_arg_names(args); - arg_names.extend(visitor.stored.iter().map(|(id, ..)| id)); - // Treat any non-arguments as "suspicious". - self.names.extend( - visitor - .loaded - .into_iter() - .filter(|(id, ..)| !arg_names.contains(id)), - ); + self.names + .extend(visitor.loaded.into_iter().filter(|loaded| { + if visitor.stored.iter().any(|stored| stored.id == loaded.id) { + return false; + } + + if includes_arg_name(&loaded.id, args) { + return false; + } + + true + })); + return; } Stmt::Return(ast::StmtReturn { @@ -132,10 +132,9 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { }) => { match func.as_ref() { Expr::Name(ast::ExprName { id, .. }) => { - let id = id.as_str(); - if id == "filter" || id == "reduce" || id == "map" { + if matches!(id.as_str(), "filter" | "reduce" | "map") { for arg in args { - if matches!(arg, Expr::Lambda(_)) { + if arg.is_lambda_expr() { self.safe_functions.push(arg); } } @@ -159,7 +158,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { for keyword in keywords { if keyword.arg.as_ref().map_or(false, |arg| arg == "key") - && matches!(keyword.value, Expr::Lambda(_)) + && keyword.value.is_lambda_expr() { self.safe_functions.push(&keyword.value); } @@ -175,17 +174,19 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { let mut visitor = LoadedNamesVisitor::default(); visitor.visit_expr(body); - // Collect all argument names. - let mut arg_names = collect_arg_names(args); - arg_names.extend(visitor.stored.iter().map(|(id, ..)| id)); - // Treat any non-arguments as "suspicious". - self.names.extend( - visitor - .loaded - .iter() - .filter(|(id, ..)| !arg_names.contains(id)), - ); + self.names + .extend(visitor.loaded.into_iter().filter(|loaded| { + if visitor.stored.iter().any(|stored| stored.id == loaded.id) { + return false; + } + + if includes_arg_name(&loaded.id, args) { + return false; + } + + true + })); return; } @@ -198,7 +199,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { #[derive(Default)] struct NamesFromAssignmentsVisitor<'a> { - names: FxHashSet<&'a str>, + names: Vec<&'a str>, } /// `Visitor` to collect all names used in an assignment expression. @@ -206,7 +207,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match expr { Expr::Name(ast::ExprName { id, .. }) => { - self.names.insert(id.as_str()); + self.names.push(id.as_str()); } Expr::Starred(ast::ExprStarred { value, .. }) => { self.visit_expr(value); @@ -223,7 +224,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> { #[derive(Default)] struct AssignedNamesVisitor<'a> { - names: FxHashSet<&'a str>, + names: Vec<&'a str>, } /// `Visitor` to collect all used identifiers in a statement. @@ -257,7 +258,7 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> { } fn visit_expr(&mut self, expr: &'a Expr) { - if matches!(expr, Expr::Lambda(_)) { + if expr.is_lambda_expr() { // Don't recurse. return; } @@ -300,15 +301,15 @@ pub(crate) fn function_uses_loop_variable<'a>(checker: &mut Checker<'a>, node: & // If a variable was used in a function or lambda body, and assigned in the // loop, flag it. - for (name, expr) in suspicious_variables { - if reassigned_in_loop.contains(name) { - if !checker.flake8_bugbear_seen.contains(&expr) { - checker.flake8_bugbear_seen.push(expr); + for name in suspicious_variables { + if reassigned_in_loop.contains(&name.id.as_str()) { + if !checker.flake8_bugbear_seen.contains(&name) { + checker.flake8_bugbear_seen.push(name); checker.diagnostics.push(Diagnostic::new( FunctionUsesLoopVariable { - name: name.to_string(), + name: name.id.to_string(), }, - expr.range(), + name.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs b/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs index 1fa1cd2634..3abcea7013 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs @@ -64,7 +64,7 @@ pub(crate) fn getattr_with_constant( func: &Expr, args: &[Expr], ) { - let Expr::Name(ast::ExprName { id, .. } )= func else { + let Expr::Name(ast::ExprName { id, .. }) = func else { return; }; if id != "getattr" { @@ -76,7 +76,8 @@ pub(crate) fn getattr_with_constant( let Expr::Constant(ast::ExprConstant { value: Constant::Str(value), .. - } )= arg else { + }) = arg + else { return; }; if !is_identifier(value) { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs index dc2a5ffbfe..4af5133187 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs @@ -69,7 +69,7 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Argume .chain(&arguments.args) .chain(&arguments.kwonlyargs) { - let Some(default)= default else { + let Some(default) = default else { continue; }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs b/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs index 225807a217..865456d3f2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs @@ -59,7 +59,11 @@ pub(crate) fn redundant_tuple_in_exception_handler( handlers: &[ExceptHandler], ) { for handler in handlers { - let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_: Some(type_), .. }) = handler else { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_: Some(type_), + .. + }) = handler + else { continue; }; let Expr::Tuple(ast::ExprTuple { elts, .. }) = type_.as_ref() else { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs b/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs index 7d1ddcaf4c..fe107f5fc0 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs @@ -82,7 +82,8 @@ pub(crate) fn setattr_with_constant( let Expr::Constant(ast::ExprConstant { value: Constant::Str(name), .. - } )= name else { + }) = name + else { return; }; if !is_identifier(name) { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs b/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs index f8cb8fa12c..dae1e2168e 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs @@ -64,7 +64,7 @@ pub(crate) fn star_arg_unpacking_after_keyword_arg( return; }; for arg in args { - let Expr::Starred (_) = arg else { + let Expr::Starred(_) = arg else { continue; }; if arg.start() <= keyword.start() { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs b/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs index 75349ba3d9..2098a2a796 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs @@ -62,7 +62,8 @@ pub(crate) fn strip_with_multi_characters( let Expr::Constant(ast::ExprConstant { value: Constant::Str(value), .. - } )= &args[0] else { + }) = &args[0] + else { return; }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs index 43356e2fa4..e9a096aacc 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs @@ -45,9 +45,9 @@ pub(crate) fn unary_prefix_increment( if !matches!(op, UnaryOp::UAdd) { return; } - let Expr::UnaryOp(ast::ExprUnaryOp { op, .. })= operand else { - return; - }; + let Expr::UnaryOp(ast::ExprUnaryOp { op, .. }) = operand else { + return; + }; if !matches!(op, UnaryOp::UAdd) { return; } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs index 58cfafa186..34e209ee48 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs @@ -63,8 +63,8 @@ pub(crate) fn unreliable_callable_check( let Expr::Constant(ast::ExprConstant { value: Constant::Str(s), .. - }) = &args[1] else - { + }) = &args[1] + else { return; }; if s != "__call__" { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index 83854bc6bc..4a4efd4e3c 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -68,7 +68,13 @@ pub(crate) fn zip_without_explicit_strict( /// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to /// `itertools.cycle` or similar). fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool { - let Expr::Call(ast::ExprCall { func, args, keywords, .. }) = &arg else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = &arg + else { return false; }; diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap index f46fd7d74e..d6a332dfc2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap @@ -1,27 +1,38 @@ --- source: crates/ruff/src/rules/flake8_bugbear/mod.rs --- -B017.py:23:9: B017 `assertRaises(Exception)` should be considered evil +B017.py:23:14: B017 `assertRaises(Exception)` should be considered evil | -21 | class Foobar(unittest.TestCase): -22 | def evil_raises(self) -> None: -23 | with self.assertRaises(Exception): - | _________^ -24 | | raise Exception("Evil I say!") - | |__________________________________________^ B017 -25 | -26 | def context_manager_raises(self) -> None: +21 | class Foobar(unittest.TestCase): +22 | def evil_raises(self) -> None: +23 | with self.assertRaises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +24 | raise Exception("Evil I say!") | -B017.py:41:5: B017 `pytest.raises(Exception)` should be considered evil +B017.py:27:14: B017 `assertRaises(BaseException)` should be considered evil | -40 | def test_pytest_raises(): -41 | with pytest.raises(Exception): - | _____^ -42 | | raise ValueError("Hello") - | |_________________________________^ B017 -43 | -44 | with pytest.raises(Exception, "hello"): +26 | def also_evil_raises(self) -> None: +27 | with self.assertRaises(BaseException): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +28 | raise Exception("Evil I say!") + | + +B017.py:45:10: B017 `pytest.raises(Exception)` should be considered evil + | +44 | def test_pytest_raises(): +45 | with pytest.raises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +46 | raise ValueError("Hello") + | + +B017.py:48:10: B017 `pytest.raises(Exception)` should be considered evil + | +46 | raise ValueError("Hello") +47 | +48 | with pytest.raises(Exception), pytest.raises(ValueError): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +49 | raise ValueError("Hello") | diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index b32379677b..1d4db80bfd 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -109,7 +109,8 @@ pub(crate) fn fix_unnecessary_generator_dict( // Extract the (k, v) from `(k, v) for ...`. let generator_exp = match_generator_exp(&arg.value)?; let tuple = match_tuple(&generator_exp.elt)?; - let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] else { + let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] + else { bail!("Expected tuple to contain two elements"); }; @@ -188,9 +189,10 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict( let tuple = match_tuple(&list_comp.elt)?; - let [Element::Simple { - value: key, .. - }, Element::Simple { value, .. }] = &tuple.elements[..] else { bail!("Expected tuple with two elements"); }; + let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] + else { + bail!("Expected tuple with two elements"); + }; tree = Expression::DictComp(Box::new(DictComp { key: Box::new(key.clone()), @@ -982,14 +984,10 @@ pub(crate) fn fix_unnecessary_map( } let Some(Element::Simple { value: key, .. }) = &tuple.elements.get(0) else { - bail!( - "Expected tuple to contain a key as the first element" - ); + bail!("Expected tuple to contain a key as the first element"); }; let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) else { - bail!( - "Expected tuple to contain a key as the second element" - ); + bail!("Expected tuple to contain a key as the second element"); }; (key, value) @@ -1063,9 +1061,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all( let call = match_call_mut(&mut tree)?; let Expression::ListComp(list_comp) = &call.args[0].value else { - bail!( - "Expected Expression::ListComp" - ); + bail!("Expected Expression::ListComp"); }; let mut new_empty_lines = vec![]; diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs index ad910495a7..71b6432f2a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs @@ -66,11 +66,13 @@ pub(crate) fn unnecessary_comprehension_any_all( if !keywords.is_empty() { return; } - let Expr::Name(ast::ExprName { id, .. } )= func else { + let Expr::Name(ast::ExprName { id, .. }) = func else { return; }; if (matches!(id.as_str(), "all" | "any")) && args.len() == 1 { - let (Expr::ListComp(ast::ExprListComp { elt, .. } )| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0] else { + let (Expr::ListComp(ast::ExprListComp { elt, .. }) + | Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0] + else { return; }; if contains_await(elt) { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs index 1ea1ae62fc..2c71d2d11a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs @@ -84,7 +84,7 @@ pub(crate) fn unnecessary_double_cast_or_process( let Some(arg) = args.first() else { return; }; - let Expr::Call(ast::ExprCall { func, ..} )= arg else { + let Expr::Call(ast::ExprCall { func, .. }) = arg else { return; }; let Some(inner) = helpers::expr_name(func) else { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs index 030eff86d3..fe8c8afb0e 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs @@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_dict( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) + else { return; }; if let Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) = argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs index d5f9cea172..470595f806 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs @@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_list( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("list", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("list", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("list") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs index 65af9cb79d..44fa61ced3 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs @@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_set( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("set") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs index e8cc9b954f..d425003f04 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs @@ -47,7 +47,9 @@ pub(crate) fn unnecessary_list_comprehension_dict( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("dict") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs index 1dc3618654..9bf415f8f8 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs @@ -47,7 +47,9 @@ pub(crate) fn unnecessary_list_comprehension_set( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("set") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs index 41e4866233..8c981d062a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs @@ -54,7 +54,9 @@ pub(crate) fn unnecessary_literal_dict( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("dict") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs index 6f3cf56fb3..43f4372404 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs @@ -55,7 +55,9 @@ pub(crate) fn unnecessary_literal_set( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("set") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs index 8a54343bbf..9de5aeec53 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs @@ -83,7 +83,7 @@ pub(crate) fn unnecessary_map( ) } - let Some(id) = helpers::expr_name(func) else { + let Some(id) = helpers::expr_name(func) else { return; }; match id { @@ -127,9 +127,11 @@ pub(crate) fn unnecessary_map( if args.len() != 2 { return; } - let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else { - return; - }; + let Some(argument) = + helpers::first_argument_with_matching_function("map", func, args) + else { + return; + }; if let Expr::Lambda(_) = argument { let mut diagnostic = create_diagnostic(id, expr.range()); if checker.patch(diagnostic.kind.rule()) { @@ -155,7 +157,9 @@ pub(crate) fn unnecessary_map( if args.len() == 1 { if let Expr::Call(ast::ExprCall { func, args, .. }) = &args[0] { - let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else { + let Some(argument) = + helpers::first_argument_with_matching_function("map", func, args) + else { return; }; if let Expr::Lambda(ast::ExprLambda { body, .. }) = argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs index 5a2c0879cc..896b9128a5 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs @@ -64,9 +64,15 @@ pub(crate) fn unnecessary_subscript_reversal( let Expr::Subscript(ast::ExprSubscript { slice, .. }) = first_arg else { return; }; - let Expr::Slice(ast::ExprSlice { lower, upper, step, range: _ }) = slice.as_ref() else { - return; - }; + let Expr::Slice(ast::ExprSlice { + lower, + upper, + step, + range: _, + }) = slice.as_ref() + else { + return; + }; if lower.is_some() || upper.is_some() { return; } @@ -77,13 +83,15 @@ pub(crate) fn unnecessary_subscript_reversal( op: UnaryOp::USub, operand, range: _, - }) = step.as_ref() else { + }) = step.as_ref() + else { return; }; let Expr::Constant(ast::ExprConstant { value: Constant::Int(val), .. - }) = operand.as_ref() else { + }) = operand.as_ref() + else { return; }; if *val != BigInt::from(1) { diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs index 200a75d0bf..f690027e80 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs @@ -49,11 +49,13 @@ pub(crate) fn call_datetime_strptime_without_zone( } }; - let (Some(grandparent), Some(parent)) = (checker.semantic().expr_grandparent(), checker.semantic().expr_parent()) else { - checker.diagnostics.push(Diagnostic::new( - CallDatetimeStrptimeWithoutZone, - location, - )); + let (Some(grandparent), Some(parent)) = ( + checker.semantic().expr_grandparent(), + checker.semantic().expr_parent(), + ) else { + checker + .diagnostics + .push(Diagnostic::new(CallDatetimeStrptimeWithoutZone, location)); return; }; diff --git a/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs b/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs index b5d3340a11..50cd33d67d 100644 --- a/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs +++ b/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs @@ -85,7 +85,7 @@ pub(crate) fn locals_in_render_function( fn is_locals_call(expr: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, .. }) = expr else { - return false + return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { matches!(call_path.as_slice(), ["", "locals"]) diff --git a/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs b/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs index a02992757e..6c30cb52c1 100644 --- a/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs +++ b/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs @@ -52,21 +52,20 @@ impl Violation for DjangoModelWithoutDunderStr { /// DJ008 pub(crate) fn model_without_dunder_str( - checker: &Checker, - bases: &[Expr], - body: &[Stmt], - class_location: &Stmt, -) -> Option { + checker: &mut Checker, + ast::StmtClassDef { + name, bases, body, .. + }: &ast::StmtClassDef, +) { if !is_non_abstract_model(bases, body, checker.semantic()) { - return None; + return; } - if !has_dunder_method(body) { - return Some(Diagnostic::new( - DjangoModelWithoutDunderStr, - class_location.range(), - )); + if has_dunder_method(body) { + return; } - None + checker + .diagnostics + .push(Diagnostic::new(DjangoModelWithoutDunderStr, name.range())); } fn has_dunder_method(body: &[Stmt]) -> bool { @@ -96,18 +95,18 @@ fn is_non_abstract_model(bases: &[Expr], body: &[Stmt], semantic: &SemanticModel /// Check if class is abstract, in terms of Django model inheritance. fn is_model_abstract(body: &[Stmt]) -> bool { for element in body.iter() { - let Stmt::ClassDef(ast::StmtClassDef {name, body, ..}) = element else { - continue + let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else { + continue; }; if name != "Meta" { continue; } for element in body.iter() { - let Stmt::Assign(ast::StmtAssign {targets, value, ..}) = element else { + let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = element else { continue; }; for target in targets.iter() { - let Expr::Name(ast::ExprName {id , ..}) = target else { + let Expr::Name(ast::ExprName { id, .. }) = target else { continue; }; if id != "abstract" { diff --git a/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs b/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs index f023431404..9e823858c0 100644 --- a/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs +++ b/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs @@ -1,8 +1,9 @@ -use rustpython_parser::ast::{self, Decorator, Expr, Ranged}; +use rustpython_parser::ast::{Decorator, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; + +use crate::checkers::ast::Checker; /// ## What it does /// Checks that Django's `@receiver` decorator is listed first, prior to @@ -48,25 +49,19 @@ impl Violation for DjangoNonLeadingReceiverDecorator { } /// DJ013 -pub(crate) fn non_leading_receiver_decorator<'a, F>( - decorator_list: &'a [Decorator], - resolve_call_path: F, -) -> Vec -where - F: Fn(&'a Expr) -> Option>, -{ - let mut diagnostics = vec![]; +pub(crate) fn non_leading_receiver_decorator(checker: &mut Checker, decorator_list: &[Decorator]) { let mut seen_receiver = false; for (i, decorator) in decorator_list.iter().enumerate() { - let is_receiver = match &decorator.expression { - Expr::Call(ast::ExprCall { func, .. }) => resolve_call_path(func) + let is_receiver = decorator.expression.as_call_expr().map_or(false, |call| { + checker + .semantic() + .resolve_call_path(&call.func) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["django", "dispatch", "receiver"]) - }), - _ => false, - }; + }) + }); if i > 0 && is_receiver && !seen_receiver { - diagnostics.push(Diagnostic::new( + checker.diagnostics.push(Diagnostic::new( DjangoNonLeadingReceiverDecorator, decorator.range(), )); @@ -77,5 +72,4 @@ where seen_receiver = true; } } - diagnostics } diff --git a/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs b/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs index a6ed90c648..b74bf19a8c 100644 --- a/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs +++ b/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs @@ -51,24 +51,14 @@ impl Violation for DjangoNullableModelStringField { } } -const NOT_NULL_TRUE_FIELDS: [&str; 6] = [ - "CharField", - "TextField", - "SlugField", - "EmailField", - "FilePathField", - "URLField", -]; - /// DJ001 -pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> Vec { - let mut errors = Vec::new(); +pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt]) { for statement in body.iter() { - let Stmt::Assign(ast::StmtAssign {value, ..}) = statement else { - continue + let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else { + continue; }; if let Some(field_name) = is_nullable_field(checker, value) { - errors.push(Diagnostic::new( + checker.diagnostics.push(Diagnostic::new( DjangoNullableModelStringField { field_name: field_name.to_string(), }, @@ -76,11 +66,10 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> V )); } } - errors } fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a str> { - let Expr::Call(ast::ExprCall {func, keywords, ..}) = value else { + let Expr::Call(ast::ExprCall { func, keywords, .. }) = value else { return None; }; @@ -88,7 +77,10 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st return None; }; - if !NOT_NULL_TRUE_FIELDS.contains(&valid_field_name) { + if !matches!( + valid_field_name, + "CharField" | "TextField" | "SlugField" | "EmailField" | "FilePathField" | "URLField" + ) { return None; } @@ -97,7 +89,7 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st let mut unique_key = false; for keyword in keywords.iter() { let Some(argument) = &keyword.arg else { - continue + continue; }; if !is_const_true(&keyword.value) { continue; diff --git a/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs b/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs index 6404a105f6..d3734537a6 100644 --- a/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs +++ b/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs @@ -63,20 +63,23 @@ use super::helpers; /// [Django Style Guide]: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#model-style #[violation] pub struct DjangoUnorderedBodyContentInModel { - elem_type: ContentType, - before: ContentType, + element_type: ContentType, + prev_element_type: ContentType, } impl Violation for DjangoUnorderedBodyContentInModel { #[derive_message_formats] fn message(&self) -> String { - let DjangoUnorderedBodyContentInModel { elem_type, before } = self; - format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {elem_type} should come before {before}") + let DjangoUnorderedBodyContentInModel { + element_type, + prev_element_type, + } = self; + format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {element_type} should come before {prev_element_type}") } } #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] -pub(crate) enum ContentType { +enum ContentType { FieldDeclaration, ManagerDeclaration, MetaClass, @@ -149,24 +152,38 @@ pub(crate) fn unordered_body_content_in_model( { return; } - let mut elements_type_found = Vec::new(); + + // Track all the element types we've seen so far. + let mut element_types = Vec::new(); + let mut prev_element_type = None; for element in body.iter() { - let Some(current_element_type) = get_element_type(element, checker.semantic()) else { + let Some(element_type) = get_element_type(element, checker.semantic()) else { continue; }; - let Some(&element_type) = elements_type_found + + // Skip consecutive elements of the same type. It's less noisy to only report + // violations at type boundaries (e.g., avoid raising a violation for _every_ + // field declaration that's out of order). + if prev_element_type == Some(element_type) { + continue; + } + + prev_element_type = Some(element_type); + + if let Some(&prev_element_type) = element_types .iter() - .find(|&&element_type| element_type > current_element_type) else { - elements_type_found.push(current_element_type); - continue; - }; - let diagnostic = Diagnostic::new( - DjangoUnorderedBodyContentInModel { - elem_type: current_element_type, - before: element_type, - }, - element.range(), - ); - checker.diagnostics.push(diagnostic); + .find(|&&prev_element_type| prev_element_type > element_type) + { + let diagnostic = Diagnostic::new( + DjangoUnorderedBodyContentInModel { + element_type, + prev_element_type, + }, + element.range(), + ); + checker.diagnostics.push(diagnostic); + } else { + element_types.push(element_type); + } } } diff --git a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap index 1af01e2856..2aae3d948b 100644 --- a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap +++ b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap @@ -1,58 +1,26 @@ --- source: crates/ruff/src/rules/flake8_django/mod.rs --- -DJ008.py:6:1: DJ008 Model does not define `__str__` method +DJ008.py:6:7: DJ008 Model does not define `__str__` method + | +5 | # Models without __str__ +6 | class TestModel1(models.Model): + | ^^^^^^^^^^ DJ008 +7 | new_field = models.CharField(max_length=10) + | + +DJ008.py:21:7: DJ008 Model does not define `__str__` method | - 5 | # Models without __str__ - 6 | / class TestModel1(models.Model): - 7 | | new_field = models.CharField(max_length=10) - 8 | | - 9 | | class Meta: -10 | | verbose_name = "test model" -11 | | verbose_name_plural = "test models" -12 | | -13 | | @property -14 | | def my_brand_new_property(self): -15 | | return 1 -16 | | -17 | | def my_beautiful_method(self): -18 | | return 2 - | |________________^ DJ008 +21 | class TestModel2(Model): + | ^^^^^^^^^^ DJ008 +22 | new_field = models.CharField(max_length=10) | -DJ008.py:21:1: DJ008 Model does not define `__str__` method +DJ008.py:36:7: DJ008 Model does not define `__str__` method | -21 | / class TestModel2(Model): -22 | | new_field = models.CharField(max_length=10) -23 | | -24 | | class Meta: -25 | | verbose_name = "test model" -26 | | verbose_name_plural = "test models" -27 | | -28 | | @property -29 | | def my_brand_new_property(self): -30 | | return 1 -31 | | -32 | | def my_beautiful_method(self): -33 | | return 2 - | |________________^ DJ008 - | - -DJ008.py:36:1: DJ008 Model does not define `__str__` method - | -36 | / class TestModel3(Model): -37 | | new_field = models.CharField(max_length=10) -38 | | -39 | | class Meta: -40 | | abstract = False -41 | | -42 | | @property -43 | | def my_brand_new_property(self): -44 | | return 1 -45 | | -46 | | def my_beautiful_method(self): -47 | | return 2 - | |________________^ DJ008 +36 | class TestModel3(Model): + | ^^^^^^^^^^ DJ008 +37 | new_field = models.CharField(max_length=10) | diff --git a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap index 835df4524c..d187a99a41 100644 --- a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap +++ b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap @@ -37,4 +37,21 @@ DJ012.py:69:5: DJ012 Order of model's inner classes, methods, and fields does no | |____________^ DJ012 | +DJ012.py:123:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class + | +121 | verbose_name = "test" +122 | +123 | first_name = models.CharField(max_length=32) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012 +124 | last_name = models.CharField(max_length=32) + | + +DJ012.py:129:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class + | +127 | pass +128 | +129 | middle_name = models.CharField(max_length=32) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012 + | + diff --git a/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs index 0f6b8c6a60..89957a02ac 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs @@ -3,6 +3,8 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + #[violation] pub struct FStringInGetTextFuncCall; @@ -14,11 +16,12 @@ impl Violation for FStringInGetTextFuncCall { } /// INT001 -pub(crate) fn f_string_in_gettext_func_call(args: &[Expr]) -> Option { +pub(crate) fn f_string_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) { if let Some(first) = args.first() { if first.is_joined_str_expr() { - return Some(Diagnostic::new(FStringInGetTextFuncCall {}, first.range())); + checker + .diagnostics + .push(Diagnostic::new(FStringInGetTextFuncCall {}, first.range())); } } - None } diff --git a/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs index ec159d0337..2f99369f25 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs @@ -3,6 +3,8 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + #[violation] pub struct FormatInGetTextFuncCall; @@ -14,15 +16,16 @@ impl Violation for FormatInGetTextFuncCall { } /// INT002 -pub(crate) fn format_in_gettext_func_call(args: &[Expr]) -> Option { +pub(crate) fn format_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) { if let Some(first) = args.first() { if let Expr::Call(ast::ExprCall { func, .. }) = &first { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() { if attr == "format" { - return Some(Diagnostic::new(FormatInGetTextFuncCall {}, first.range())); + checker + .diagnostics + .push(Diagnostic::new(FormatInGetTextFuncCall {}, first.range())); } } } } - None } diff --git a/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs index 088eaa60f8..eab5d74c93 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs @@ -1,5 +1,6 @@ use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -14,7 +15,7 @@ impl Violation for PrintfInGetTextFuncCall { } /// INT003 -pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option { +pub(crate) fn printf_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) { if let Some(first) = args.first() { if let Expr::BinOp(ast::ExprBinOp { op: Operator::Mod { .. }, @@ -27,9 +28,10 @@ pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option { .. }) = left.as_ref() { - return Some(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range())); + checker + .diagnostics + .push(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range())); } } } - None } diff --git a/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs b/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs index 2905bf8d34..05893da8e5 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs @@ -60,7 +60,12 @@ impl AlwaysAutofixableViolation for MultipleStartsEndsWith { /// PIE810 pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: BoolOp::Or, values, range: _ }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::Or, + values, + range: _, + }) = expr + else { return; }; @@ -70,24 +75,25 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { func, args, keywords, - range: _ - }) = &call else { - continue + range: _, + }) = &call + else { + continue; }; if !(args.len() == 1 && keywords.is_empty()) { continue; } - let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func.as_ref() else { - continue + let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else { + continue; }; if attr != "startswith" && attr != "endswith" { continue; } - let Expr::Name(ast::ExprName { id: arg_name, .. } )= value.as_ref() else { - continue + let Expr::Name(ast::ExprName { id: arg_name, .. }) = value.as_ref() else { + continue; }; duplicates @@ -110,8 +116,17 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { .iter() .map(|index| &values[*index]) .map(|expr| { - let Expr::Call(ast::ExprCall { func: _, args, keywords: _, range: _}) = expr else { - unreachable!("{}", format!("Indices should only contain `{attr_name}` calls")) + let Expr::Call(ast::ExprCall { + func: _, + args, + keywords: _, + range: _, + }) = expr + else { + unreachable!( + "{}", + format!("Indices should only contain `{attr_name}` calls") + ) }; args.get(0) .unwrap_or_else(|| panic!("`{attr_name}` should have one argument")) diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index b790c3c77e..5575a7ced5 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -22,6 +22,8 @@ mod tests { #[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))] #[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))] #[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))] + #[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.py"))] + #[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.pyi"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))] #[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))] @@ -56,6 +58,8 @@ mod tests { #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.pyi"))] #[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.py"))] #[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))] + #[test_case(Rule::PatchVersionComparison, Path::new("PYI004.py"))] + #[test_case(Rule::PatchVersionComparison, Path::new("PYI004.pyi"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))] #[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))] @@ -72,6 +76,10 @@ mod tests { #[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.pyi"))] #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.py"))] #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.pyi"))] + #[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.py"))] + #[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.pyi"))] + #[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.py"))] + #[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.pyi"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs b/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs index 8d43070991..332ddfc5f7 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{CmpOp, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -52,19 +52,23 @@ pub struct BadVersionInfoComparison; impl Violation for BadVersionInfoComparison { #[derive_message_formats] fn message(&self) -> String { - format!("Use `<` or `>=` for version info comparisons") + format!("Use `<` or `>=` for `sys.version_info` comparisons") } } /// PYI006 -pub(crate) fn bad_version_info_comparison( - checker: &mut Checker, - expr: &Expr, - left: &Expr, - ops: &[CmpOp], - comparators: &[Expr], -) { - let ([op], [_right]) = (ops, comparators) else { +pub(crate) fn bad_version_info_comparison(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + .. + }) = test + else { + return; + }; + + let ([op], [_right]) = (ops.as_slice(), comparators.as_slice()) else { return; }; @@ -78,8 +82,11 @@ pub(crate) fn bad_version_info_comparison( return; } - if !matches!(op, CmpOp::Lt | CmpOp::GtE) { - let diagnostic = Diagnostic::new(BadVersionInfoComparison, expr.range()); - checker.diagnostics.push(diagnostic); + if matches!(op, CmpOp::Lt | CmpOp::GtE) { + return; } + + checker + .diagnostics + .push(Diagnostic::new(BadVersionInfoComparison, test.range())); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs new file mode 100644 index 0000000000..a8287ff671 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs @@ -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())); +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs b/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs index 09347b72d2..e4971016b1 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs @@ -70,15 +70,12 @@ pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &De kind: MemberKind::Method, stmt, .. - }) = definition else { + }) = definition + else { return; }; - let Stmt::FunctionDef(ast::StmtFunctionDef { - name, - returns, - .. - }) = stmt else { + let Stmt::FunctionDef(ast::StmtFunctionDef { name, returns, .. }) = stmt else { return; }; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 6286ebdca3..612055db37 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -1,6 +1,7 @@ pub(crate) use any_eq_ne_annotation::*; pub(crate) use bad_version_info_comparison::*; pub(crate) use collections_named_tuple::*; +pub(crate) use complex_if_statement_in_stub::*; pub(crate) use docstring_in_stubs::*; pub(crate) use duplicate_union_member::*; pub(crate) use ellipsis_in_non_empty_class_body::*; @@ -22,10 +23,12 @@ pub(crate) use type_alias_naming::*; pub(crate) use type_comment_in_stub::*; pub(crate) use unaliased_collections_abc_set_import::*; pub(crate) use unrecognized_platform::*; +pub(crate) use unrecognized_version_info::*; mod any_eq_ne_annotation; mod bad_version_info_comparison; mod collections_named_tuple; +mod complex_if_statement_in_stub; mod docstring_in_stubs; mod duplicate_union_member; mod ellipsis_in_non_empty_class_body; @@ -47,3 +50,4 @@ mod type_alias_naming; mod type_comment_in_stub; mod unaliased_collections_abc_set_import; mod unrecognized_platform; +mod unrecognized_version_info; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs b/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs index 07f62306b7..5a17dddcf7 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs @@ -70,17 +70,30 @@ pub(crate) fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: & }; if let Expr::Call(ast::ExprCall { func, .. }) = value { - let Some(kind) = checker.semantic().resolve_call_path(func).and_then(|call_path| { - if checker.semantic().match_typing_call_path(&call_path, "ParamSpec") { - Some(VarKind::ParamSpec) - } else if checker.semantic().match_typing_call_path(&call_path, "TypeVar") { - Some(VarKind::TypeVar) - } else if checker.semantic().match_typing_call_path(&call_path, "TypeVarTuple") { - Some(VarKind::TypeVarTuple) - } else { - None - } - }) else { + let Some(kind) = checker + .semantic() + .resolve_call_path(func) + .and_then(|call_path| { + if checker + .semantic() + .match_typing_call_path(&call_path, "ParamSpec") + { + Some(VarKind::ParamSpec) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVar") + { + Some(VarKind::TypeVar) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVarTuple") + { + Some(VarKind::TypeVarTuple) + } else { + None + } + }) + else { return; }; checker diff --git a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs index eacd3a98dd..b1ed3595b0 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -270,7 +270,7 @@ fn is_valid_default_value_without_annotation(default: &Expr) -> bool { /// Returns `true` if an [`Expr`] appears to be `TypeVar`, `TypeVarTuple`, `NewType`, or `ParamSpec` /// call. fn is_type_var_like_call(expr: &Expr, semantic: &SemanticModel) -> bool { - let Expr::Call(ast::ExprCall { func, .. } )= expr else { + let Expr::Call(ast::ExprCall { func, .. }) = expr else { return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { @@ -298,6 +298,16 @@ fn is_special_assignment(target: &Expr, semantic: &SemanticModel) -> bool { } } +/// Returns `true` if this is an assignment to a simple `Final`-annotated variable. +fn is_final_assignment(annotation: &Expr, value: &Expr, semantic: &SemanticModel) -> bool { + if matches!(value, Expr::Name(_) | Expr::Attribute(_)) { + if semantic.match_typing_expr(annotation, "Final") { + return true; + } + } + false +} + /// Returns `true` if the a class is an enum, based on its base classes. fn is_enum(bases: &[Expr], semantic: &SemanticModel) -> bool { return bases.iter().any(|expr| { @@ -438,6 +448,9 @@ pub(crate) fn annotated_assignment_default_in_stub( if is_type_var_like_call(value, checker.semantic()) { return; } + if is_final_assignment(annotation, value, checker.semantic()) { + return; + } if is_valid_default_value_with_annotation(value, true, checker.locator, checker.semantic()) { return; } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs index 47d1f9f177..daa63da702 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs @@ -50,8 +50,9 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { returns, args, .. - }) = stmt else { - return + }) = stmt + else { + return; }; let Some(returns) = returns else { diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs index af876baa0c..418c0c55bf 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -89,19 +89,21 @@ impl Violation for UnrecognizedPlatformName { } /// PYI007, PYI008 -pub(crate) fn unrecognized_platform( - checker: &mut Checker, - expr: &Expr, - left: &Expr, - ops: &[CmpOp], - comparators: &[Expr], -) { - let ([op], [right]) = (ops, comparators) else { +pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + .. + }) = test + else { + return; + }; + + let ([op], [right]) = (ops.as_slice(), comparators.as_slice()) else { return; }; - let diagnostic_unrecognized_platform_check = - Diagnostic::new(UnrecognizedPlatformCheck, expr.range()); if !checker .semantic() .resolve_call_path(left) @@ -113,23 +115,24 @@ pub(crate) fn unrecognized_platform( } // "in" might also make sense but we don't currently have one. - if !matches!(op, CmpOp::Eq | CmpOp::NotEq) && checker.enabled(Rule::UnrecognizedPlatformCheck) { - checker - .diagnostics - .push(diagnostic_unrecognized_platform_check); + if !matches!(op, CmpOp::Eq | CmpOp::NotEq) { + if checker.enabled(Rule::UnrecognizedPlatformCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedPlatformCheck, test.range())); + } return; } - match right { - Expr::Constant(ast::ExprConstant { - value: Constant::Str(value), - .. - }) => { - // Other values are possible but we don't need them right now. - // This protects against typos. - if !["linux", "win32", "cygwin", "darwin"].contains(&value.as_str()) - && checker.enabled(Rule::UnrecognizedPlatformName) - { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(value), + .. + }) = right + { + // Other values are possible but we don't need them right now. + // This protects against typos. + if checker.enabled(Rule::UnrecognizedPlatformName) { + if !matches!(value.as_str(), "linux" | "win32" | "cygwin" | "darwin") { checker.diagnostics.push(Diagnostic::new( UnrecognizedPlatformName { platform: value.clone(), @@ -138,12 +141,11 @@ pub(crate) fn unrecognized_platform( )); } } - _ => { - if checker.enabled(Rule::UnrecognizedPlatformCheck) { - checker - .diagnostics - .push(diagnostic_unrecognized_platform_check); - } + } else { + if checker.enabled(Rule::UnrecognizedPlatformCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedPlatformCheck, test.range())); } } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_version_info.rs b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_version_info.rs new file mode 100644 index 0000000000..dfb288154c --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_version_info.rs @@ -0,0 +1,283 @@ +use num_bigint::BigInt; +use num_traits::{One, Zero}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::map_subscript; + +use crate::checkers::ast::Checker; +use crate::registry::Rule; + +/// ## What it does +/// Checks for problematic `sys.version_info`-related conditions in stubs. +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions using `sys.version_info`. However, there are a number of common +/// mistakes involving `sys.version_info` comparisons that should be avoided. +/// For example, comparing against a string can lead to unexpected behavior. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info[0] == "2": +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info[0] == 2: +/// ... +/// ``` +#[violation] +pub struct UnrecognizedVersionInfoCheck; + +impl Violation for UnrecognizedVersionInfoCheck { + #[derive_message_formats] + fn message(&self) -> String { + format!("Unrecognized `sys.version_info` check") + } +} + +/// ## What it does +/// Checks for Python version comparisons in stubs that compare against patch +/// versions (e.g., Python 3.8.3) instead of major and minor versions (e.g., +/// Python 3.8). +/// +/// ## 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. In particular, type checkers don't support +/// patch versions (e.g., Python 3.8.3), only major and minor versions (e.g., +/// Python 3.8). Therefore, version checks in stubs should only use the major +/// and minor versions. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info >= (3, 4, 3): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info >= (3, 4): +/// ... +/// ``` +#[violation] +pub struct PatchVersionComparison; + +impl Violation for PatchVersionComparison { + #[derive_message_formats] + fn message(&self) -> String { + format!("Version comparison must use only major and minor version") + } +} + +/// ## What it does +/// Checks for Python version comparisons that compare against a tuple of the +/// wrong length. +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions and platforms. When comparing against `sys.version_info`, avoid +/// comparing against tuples of the wrong length, which can lead to unexpected +/// behavior. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info[:2] == (3,): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info[0] == 3: +/// ... +/// ``` +#[violation] +pub struct WrongTupleLengthVersionComparison { + expected_length: usize, +} + +impl Violation for WrongTupleLengthVersionComparison { + #[derive_message_formats] + fn message(&self) -> String { + let WrongTupleLengthVersionComparison { expected_length } = self; + format!("Version comparison must be against a length-{expected_length} tuple") + } +} + +/// PYI003, PYI004, PYI005 +pub(crate) fn unrecognized_version_info(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + .. + }) = test + else { + return; + }; + + let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) else { + return; + }; + + if !checker + .semantic() + .resolve_call_path(map_subscript(left)) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["sys", "version_info"]) + }) + { + return; + } + + if let Some(expected) = ExpectedComparator::try_from(left) { + version_check(checker, expected, test, *op, comparator); + } else { + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + } +} + +fn version_check( + checker: &mut Checker, + expected: ExpectedComparator, + test: &Expr, + op: CmpOp, + comparator: &Expr, +) { + // Single digit comparison, e.g., `sys.version_info[0] == 2`. + if expected == ExpectedComparator::MajorDigit { + if !is_int_constant(comparator) { + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + } + return; + } + + // Tuple comparison, e.g., `sys.version_info == (3, 4)`. + let Expr::Tuple(ast::ExprTuple { elts, .. }) = comparator else { + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + return; + }; + + if !elts.iter().all(is_int_constant) { + // All tuple elements must be integers, e.g., `sys.version_info == (3, 4)` instead of + // `sys.version_info == (3.0, 4)`. + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + } else if elts.len() > 2 { + // Must compare against major and minor version only, e.g., `sys.version_info == (3, 4)` + // instead of `sys.version_info == (3, 4, 0)`. + if checker.enabled(Rule::PatchVersionComparison) { + checker + .diagnostics + .push(Diagnostic::new(PatchVersionComparison, test.range())); + } + } + + if checker.enabled(Rule::WrongTupleLengthVersionComparison) { + if op == CmpOp::Eq || op == CmpOp::NotEq { + let expected_length = match expected { + ExpectedComparator::MajorTuple => 1, + ExpectedComparator::MajorMinorTuple => 2, + _ => return, + }; + + if elts.len() != expected_length { + checker.diagnostics.push(Diagnostic::new( + WrongTupleLengthVersionComparison { expected_length }, + test.range(), + )); + } + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +enum ExpectedComparator { + MajorDigit, + MajorTuple, + MajorMinorTuple, + AnyTuple, +} + +impl ExpectedComparator { + /// Returns the expected comparator for the given expression, if any. + fn try_from(expr: &Expr) -> Option { + let Expr::Subscript(ast::ExprSubscript { slice, .. }) = expr else { + return Some(ExpectedComparator::AnyTuple); + }; + + // Only allow: (1) simple slices of the form `[:n]`, or (2) explicit indexing into the first + // element (major version) of the tuple. + match slice.as_ref() { + Expr::Slice(ast::ExprSlice { + lower: None, + upper: Some(upper), + step: None, + .. + }) => { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Int(upper), + .. + }) = upper.as_ref() + { + if *upper == BigInt::one() { + return Some(ExpectedComparator::MajorTuple); + } + if *upper == BigInt::from(2) { + return Some(ExpectedComparator::MajorMinorTuple); + } + } + } + Expr::Constant(ast::ExprConstant { + value: Constant::Int(n), + .. + }) if n.is_zero() => { + return Some(ExpectedComparator::MajorDigit); + } + _ => (), + } + + None + } +} + +/// Returns `true` if the given expression is an integer constant. +fn is_int_constant(expr: &Expr) -> bool { + matches!( + expr, + Expr::Constant(ast::ExprConstant { + value: ast::Constant::Int(_), + .. + }) + ) +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap new file mode 100644 index 0000000000..103bef4bac --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI002.pyi:3:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +1 | import sys +2 | +3 | if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI002 +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | + +PYI002.pyi:4:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +3 | if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI002 +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +6 | if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | + +PYI002.pyi:5:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +3 | if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^^^^^^ PYI002 +6 | if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | + +PYI002.pyi:6:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +6 | if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^ PYI002 + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.pyi.snap new file mode 100644 index 0000000000..2ce520c09b --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.pyi.snap @@ -0,0 +1,173 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI003.pyi:4:4: PYI003 Unrecognized `sys.version_info` check + | +3 | if sys.version_info[0] == 2: ... +4 | 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:' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:5:4: PYI003 Unrecognized `sys.version_info` check + | +3 | if sys.version_info[0] == 2: ... +4 | 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:' +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check +7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:6:4: PYI003 Unrecognized `sys.version_info` check + | +4 | 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:' +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check +8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:7:4: PYI003 Unrecognized `sys.version_info` check + | +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check +7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check +9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:8:4: PYI003 Unrecognized `sys.version_info` check + | + 6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check + 7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + 8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:9:4: PYI003 Unrecognized `sys.version_info` check + | + 7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + 8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:10:4: PYI003 Unrecognized `sys.version_info` check + | + 8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:11:4: PYI003 Unrecognized `sys.version_info` check + | + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:12:4: PYI003 Unrecognized `sys.version_info` check + | +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +14 | if sys.version_info[:1] == (2,): ... + | + +PYI003.pyi:13:4: PYI003 Unrecognized `sys.version_info` check + | +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +14 | if sys.version_info[:1] == (2,): ... +15 | if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:15:4: PYI003 Unrecognized `sys.version_info` check + | +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +14 | if sys.version_info[:1] == (2,): ... +15 | if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +16 | if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple +17 | if sys.version_info[:2] == (2, 7): ... + | + +PYI003.pyi:19:4: PYI003 Unrecognized `sys.version_info` check + | +17 | if sys.version_info[:2] == (2, 7): ... +18 | if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple +19 | if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:20:4: PYI003 Unrecognized `sys.version_info` check + | +18 | if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple +19 | if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:21:4: PYI003 Unrecognized `sys.version_info` check + | +19 | if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:22:4: PYI003 Unrecognized `sys.version_info` check + | +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check +24 | if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:23:4: PYI003 Unrecognized `sys.version_info` check + | +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +24 | if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check +25 | if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version + | + +PYI003.pyi:24:4: PYI003 Unrecognized `sys.version_info` check + | +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check +24 | if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +25 | if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version +26 | if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.pyi.snap new file mode 100644 index 0000000000..ddb37572e5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.pyi.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI004.pyi:4:4: PYI004 Version comparison must use only major and minor version + | +2 | from sys import version_info +3 | +4 | if sys.version_info >= (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +5 | if sys.version_info < (3, 4, 3): ... # PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 + | + +PYI004.pyi:5:4: PYI004 Version comparison must use only major and minor version + | +4 | if sys.version_info >= (3, 4, 3): ... # PYI004 +5 | if sys.version_info < (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 +7 | if sys.version_info != (3, 4, 3): ... # PYI004 + | + +PYI004.pyi:6:4: PYI004 Version comparison must use only major and minor version + | +4 | if sys.version_info >= (3, 4, 3): ... # PYI004 +5 | if sys.version_info < (3, 4, 3): ... # PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +7 | if sys.version_info != (3, 4, 3): ... # PYI004 + | + +PYI004.pyi:7:4: PYI004 Version comparison must use only major and minor version + | +5 | if sys.version_info < (3, 4, 3): ... # PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 +7 | if sys.version_info != (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +8 | +9 | if sys.version_info[0] == 2: ... + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap new file mode 100644 index 0000000000..1641bce44c --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI005.pyi:4:4: PYI005 Version comparison must be against a length-1 tuple + | +2 | from sys import platform, version_info +3 | +4 | if sys.version_info[:1] == (2, 7): ... # Y005 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI005 +5 | if sys.version_info[:2] == (2,): ... # Y005 + | + +PYI005.pyi:5:4: PYI005 Version comparison must be against a length-2 tuple + | +4 | if sys.version_info[:1] == (2, 7): ... # Y005 +5 | if sys.version_info[:2] == (2,): ... # Y005 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI005 + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap index 3c0293215f..8dbd74304f 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- -PYI006.pyi:8:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:8:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 6 | if sys.version_info >= (3, 9): ... # OK 7 | @@ -11,7 +11,7 @@ PYI006.pyi:8:4: PYI006 Use `<` or `>=` for version info comparisons 10 | if sys.version_info == (3, 9): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:10:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:10:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 8 | if sys.version_info == (3, 9): ... # OK 9 | @@ -21,7 +21,7 @@ PYI006.pyi:10:4: PYI006 Use `<` or `>=` for version info comparisons 12 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:12:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:12:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 10 | if sys.version_info == (3, 9): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 11 | @@ -31,7 +31,7 @@ PYI006.pyi:12:4: PYI006 Use `<` or `>=` for version info comparisons 14 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:14:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:14:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 12 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 13 | @@ -41,7 +41,7 @@ PYI006.pyi:14:4: PYI006 Use `<` or `>=` for version info comparisons 16 | if sys.version_info > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:16:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:16:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 14 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 15 | @@ -51,7 +51,7 @@ PYI006.pyi:16:4: PYI006 Use `<` or `>=` for version info comparisons 18 | if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:18:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:18:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 16 | if sys.version_info > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 17 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index c4272d4a5f..363b327c8c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -226,10 +226,10 @@ pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) { } /// PT017 -pub(crate) fn assert_in_exception_handler(handlers: &[ExceptHandler]) -> Vec { - handlers - .iter() - .flat_map(|handler| match handler { +pub(crate) fn assert_in_exception_handler(checker: &mut Checker, handlers: &[ExceptHandler]) { + checker + .diagnostics + .extend(handlers.iter().flat_map(|handler| match handler { ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) => { @@ -239,8 +239,7 @@ pub(crate) fn assert_in_exception_handler(handlers: &[ExceptHandler]) -> Vec let statements = if outer_indent.is_empty() { &mut tree.body } else { - let [Statement::Compound(CompoundStatement::FunctionDef(embedding))] = &mut *tree.body else { + let [Statement::Compound(CompoundStatement::FunctionDef(embedding))] = &mut *tree.body + else { bail!("Expected statement to be embedded in a function definition") }; diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs index 7cf8ff64f9..70aad572bc 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -1,17 +1,15 @@ use std::fmt; -use anyhow::Result; -use ruff_text_size::{TextLen, TextRange, TextSize}; +use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast::Decorator; -use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Expr, Keyword, Ranged, Stmt}; +use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; -use ruff_python_ast::helpers::collect_arg_names; +use ruff_python_ast::helpers::includes_arg_name; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_semantic::analyze::visibility::is_abstract; @@ -25,21 +23,6 @@ use super::helpers::{ get_mark_decorators, is_pytest_fixture, is_pytest_yield_fixture, keyword_is_literal, }; -#[derive(Debug, PartialEq, Eq)] -pub(crate) enum Parentheses { - None, - Empty, -} - -impl fmt::Display for Parentheses { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - Parentheses::None => fmt.write_str(""), - Parentheses::Empty => fmt.write_str("()"), - } - } -} - #[violation] pub struct PytestFixtureIncorrectParenthesesStyle { expected: Parentheses, @@ -196,8 +179,23 @@ impl AlwaysAutofixableViolation for PytestUnnecessaryAsyncioMarkOnFixture { } } -#[derive(Default)] +#[derive(Debug, PartialEq, Eq)] +enum Parentheses { + None, + Empty, +} + +impl fmt::Display for Parentheses { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Parentheses::None => fmt.write_str(""), + Parentheses::Empty => fmt.write_str("()"), + } + } +} + /// Visitor that skips functions +#[derive(Debug, Default)] struct SkipFunctionsVisitor<'a> { has_return_with_value: bool, has_yield_from: bool, @@ -245,7 +243,7 @@ where } } -fn get_fixture_decorator<'a>( +fn fixture_decorator<'a>( decorators: &'a [Decorator], semantic: &SemanticModel, ) -> Option<&'a Decorator> { @@ -271,16 +269,6 @@ fn pytest_fixture_parentheses( checker.diagnostics.push(diagnostic); } -pub(crate) fn fix_extraneous_scope_function( - locator: &Locator, - stmt_at: TextSize, - expr_range: TextRange, - args: &[Expr], - keywords: &[Keyword], -) -> Result { - remove_argument(locator, stmt_at, expr_range, args, keywords, false) -} - /// PT001, PT002, PT003 fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &Decorator) { match &decorator.expression { @@ -290,28 +278,31 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D keywords, range: _, }) => { - if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) - && !checker.settings.flake8_pytest_style.fixture_parentheses - && args.is_empty() - && keywords.is_empty() - { - let fix = Fix::automatic(Edit::deletion(func.end(), decorator.end())); - pytest_fixture_parentheses( - checker, - decorator, - fix, - Parentheses::None, - Parentheses::Empty, - ); + if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { + if !checker.settings.flake8_pytest_style.fixture_parentheses + && args.is_empty() + && keywords.is_empty() + { + let fix = Fix::automatic(Edit::deletion(func.end(), decorator.end())); + pytest_fixture_parentheses( + checker, + decorator, + fix, + Parentheses::None, + Parentheses::Empty, + ); + } } - if checker.enabled(Rule::PytestFixturePositionalArgs) && !args.is_empty() { - checker.diagnostics.push(Diagnostic::new( - PytestFixturePositionalArgs { - function: func_name.to_string(), - }, - decorator.range(), - )); + if checker.enabled(Rule::PytestFixturePositionalArgs) { + if !args.is_empty() { + checker.diagnostics.push(Diagnostic::new( + PytestFixturePositionalArgs { + function: func_name.to_string(), + }, + decorator.range(), + )); + } } if checker.enabled(Rule::PytestExtraneousScopeFunction) { @@ -324,16 +315,16 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D let mut diagnostic = Diagnostic::new(PytestExtraneousScopeFunction, scope_keyword.range()); if checker.patch(diagnostic.kind.rule()) { - let expr_range = diagnostic.range(); - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - fix_extraneous_scope_function( + diagnostic.try_set_fix(|| { + remove_argument( checker.locator, - decorator.start(), - expr_range, + func.end(), + scope_keyword.range, args, keywords, + false, ) + .map(Fix::suggested) }); } checker.diagnostics.push(diagnostic); @@ -342,20 +333,20 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D } } _ => { - if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) - && checker.settings.flake8_pytest_style.fixture_parentheses - { - let fix = Fix::automatic(Edit::insertion( - Parentheses::Empty.to_string(), - decorator.end(), - )); - pytest_fixture_parentheses( - checker, - decorator, - fix, - Parentheses::Empty, - Parentheses::None, - ); + if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { + if checker.settings.flake8_pytest_style.fixture_parentheses { + let fix = Fix::automatic(Edit::insertion( + Parentheses::Empty.to_string(), + decorator.end(), + )); + pytest_fixture_parentheses( + checker, + decorator, + fix, + Parentheses::Empty, + Parentheses::None, + ); + } } } } @@ -455,7 +446,7 @@ fn check_fixture_decorator_name(checker: &mut Checker, decorator: &Decorator) { /// PT021 fn check_fixture_addfinalizer(checker: &mut Checker, args: &Arguments, body: &[Stmt]) { - if !collect_arg_names(args).contains(&"request") { + if !includes_arg_name("request", args) { return; } @@ -511,7 +502,7 @@ pub(crate) fn fixture( decorators: &[Decorator], body: &[Stmt], ) { - let decorator = get_fixture_decorator(decorators, checker.semantic()); + let decorator = fixture_decorator(decorators, checker.semantic()); if let Some(decorator) = decorator { if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) || checker.enabled(Rule::PytestFixturePositionalArgs) diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs index 7fb2da8f1f..2e09517978 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs @@ -1,10 +1,9 @@ -use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; +use rustpython_parser::ast::{self, Arguments, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; -use ruff_python_ast::helpers::{collect_arg_names, SimpleCallArgs}; +use ruff_python_ast::helpers::{includes_arg_name, SimpleCallArgs}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -18,10 +17,10 @@ impl Violation for PytestPatchWithLambda { } } -#[derive(Default)] /// Visitor that checks references the argument names in the lambda body. +#[derive(Debug)] struct LambdaBodyVisitor<'a> { - names: FxHashSet<&'a str>, + arguments: &'a Arguments, uses_args: bool, } @@ -32,11 +31,15 @@ where fn visit_expr(&mut self, expr: &'b Expr) { match expr { Expr::Name(ast::ExprName { id, .. }) => { - if self.names.contains(&id.as_str()) { + if includes_arg_name(id, self.arguments) { self.uses_args = true; } } - _ => visitor::walk_expr(self, expr), + _ => { + if !self.uses_args { + visitor::walk_expr(self, expr); + } + } } } } @@ -60,7 +63,7 @@ fn check_patch_call( { // Walk the lambda body. let mut visitor = LambdaBodyVisitor { - names: collect_arg_names(args), + arguments: args, uses_args: false, }; visitor.visit_expr(body); diff --git a/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs b/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs index 6f0000e92c..710dec32cc 100644 --- a/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs +++ b/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs @@ -42,16 +42,16 @@ impl AlwaysAutofixableViolation for BadQuotesInlineString { fn message(&self) -> String { let BadQuotesInlineString { quote } = self; match quote { - Quote::Single => format!("Double quotes found but single quotes preferred"), Quote::Double => format!("Single quotes found but double quotes preferred"), + Quote::Single => format!("Double quotes found but single quotes preferred"), } } fn autofix_title(&self) -> String { let BadQuotesInlineString { quote } = self; match quote { - Quote::Single => "Replace double quotes with single quotes".to_string(), Quote::Double => "Replace single quotes with double quotes".to_string(), + Quote::Single => "Replace double quotes with single quotes".to_string(), } } } @@ -91,16 +91,16 @@ impl AlwaysAutofixableViolation for BadQuotesMultilineString { fn message(&self) -> String { let BadQuotesMultilineString { quote } = self; match quote { - Quote::Single => format!("Double quote multiline found but single quotes preferred"), Quote::Double => format!("Single quote multiline found but double quotes preferred"), + Quote::Single => format!("Double quote multiline found but single quotes preferred"), } } fn autofix_title(&self) -> String { let BadQuotesMultilineString { quote } = self; match quote { - Quote::Single => "Replace double multiline quotes with single quotes".to_string(), Quote::Double => "Replace single multiline quotes with double quotes".to_string(), + Quote::Single => "Replace double multiline quotes with single quotes".to_string(), } } } @@ -139,16 +139,16 @@ impl AlwaysAutofixableViolation for BadQuotesDocstring { fn message(&self) -> String { let BadQuotesDocstring { quote } = self; match quote { - Quote::Single => format!("Double quote docstring found but single quotes preferred"), Quote::Double => format!("Single quote docstring found but double quotes preferred"), + Quote::Single => format!("Double quote docstring found but single quotes preferred"), } } fn autofix_title(&self) -> String { let BadQuotesDocstring { quote } = self; match quote { - Quote::Single => "Replace double quotes docstring with single quotes".to_string(), Quote::Double => "Replace single quotes docstring with double quotes".to_string(), + Quote::Single => "Replace double quotes docstring with single quotes".to_string(), } } } @@ -186,8 +186,8 @@ impl AlwaysAutofixableViolation for AvoidableEscapedQuote { const fn good_single(quote: Quote) -> char { match quote { - Quote::Single => '\'', Quote::Double => '"', + Quote::Single => '\'', } } @@ -200,22 +200,22 @@ const fn bad_single(quote: Quote) -> char { const fn good_multiline(quote: Quote) -> &'static str { match quote { - Quote::Single => "'''", Quote::Double => "\"\"\"", + Quote::Single => "'''", } } const fn good_multiline_ending(quote: Quote) -> &'static str { match quote { - Quote::Single => "'\"\"\"", Quote::Double => "\"'''", + Quote::Single => "'\"\"\"", } } const fn good_docstring(quote: Quote) -> &'static str { match quote { - Quote::Single => "'", Quote::Double => "\"", + Quote::Single => "'", } } diff --git a/crates/ruff/src/rules/flake8_quotes/settings.rs b/crates/ruff/src/rules/flake8_quotes/settings.rs index 121501065e..d0f377a2fb 100644 --- a/crates/ruff/src/rules/flake8_quotes/settings.rs +++ b/crates/ruff/src/rules/flake8_quotes/settings.rs @@ -8,10 +8,10 @@ use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum Quote { - /// Use single quotes. - Single, /// Use double quotes. Double, + /// Use single quotes. + Single, } impl Default for Quote { diff --git a/crates/ruff/src/rules/flake8_return/rules/function.rs b/crates/ruff/src/rules/flake8_return/rules/function.rs index 11b5582579..01d27636d3 100644 --- a/crates/ruff/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff/src/rules/flake8_return/rules/function.rs @@ -481,7 +481,10 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) { continue; }; - let Expr::Name(ast::ExprName { id: returned_id, .. }) = value.as_ref() else { + let Expr::Name(ast::ExprName { + id: returned_id, .. + }) = value.as_ref() + else { continue; }; @@ -494,7 +497,10 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) { continue; }; - let Expr::Name(ast::ExprName { id: assigned_id, .. }) = target else { + let Expr::Name(ast::ExprName { + id: assigned_id, .. + }) = target + else { continue; }; diff --git a/crates/ruff/src/rules/flake8_return/visitor.rs b/crates/ruff/src/rules/flake8_return/visitor.rs index d07c12b184..32b7daa93d 100644 --- a/crates/ruff/src/rules/flake8_return/visitor.rs +++ b/crates/ruff/src/rules/flake8_return/visitor.rs @@ -5,27 +5,27 @@ use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; #[derive(Default)] -pub(crate) struct Stack<'a> { +pub(super) struct Stack<'a> { /// The `return` statements in the current function. - pub(crate) returns: Vec<&'a ast::StmtReturn>, + pub(super) returns: Vec<&'a ast::StmtReturn>, /// The `else` statements in the current function. - pub(crate) elses: Vec<&'a ast::StmtIf>, + pub(super) elses: Vec<&'a ast::StmtIf>, /// The `elif` statements in the current function. - pub(crate) elifs: Vec<&'a ast::StmtIf>, + pub(super) elifs: Vec<&'a ast::StmtIf>, /// The non-local variables in the current function. - pub(crate) non_locals: FxHashSet<&'a str>, + pub(super) non_locals: FxHashSet<&'a str>, /// Whether the current function is a generator. - pub(crate) is_generator: bool, + pub(super) is_generator: bool, /// The `assignment`-to-`return` statement pairs in the current function. /// TODO(charlie): Remove the extra [`Stmt`] here, which is necessary to support statement /// removal for the `return` statement. - pub(crate) assignment_return: Vec<(&'a ast::StmtAssign, &'a ast::StmtReturn, &'a Stmt)>, + pub(super) assignment_return: Vec<(&'a ast::StmtAssign, &'a ast::StmtReturn, &'a Stmt)>, } #[derive(Default)] -pub(crate) struct ReturnVisitor<'a> { +pub(super) struct ReturnVisitor<'a> { /// The current stack of nodes. - pub(crate) stack: Stack<'a>, + pub(super) stack: Stack<'a>, /// The preceding sibling of the current node. sibling: Option<&'a Stmt>, /// The parent nodes of the current node. diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs index 4bf4e77d08..df882a4b0e 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs @@ -299,7 +299,12 @@ fn is_same_expr<'a>(a: &'a Expr, b: &'a Expr) -> Option<&'a str> { /// SIM101 pub(crate) fn duplicate_isinstance_call(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; }; @@ -308,7 +313,13 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { let mut duplicates: FxHashMap> = FxHashMap::default(); for (index, call) in values.iter().enumerate() { // Verify that this is an `isinstance` call. - let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &call else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + range: _, + }) = &call + else { continue; }; if args.len() != 2 { @@ -430,7 +441,13 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { } fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> { - let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _ } )= expr else { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = expr + else { return None; }; if ops.len() != 1 || comparators.len() != 1 { @@ -451,7 +468,12 @@ fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> { /// SIM109 pub(crate) fn compare_with_tuple(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; }; @@ -540,7 +562,12 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { /// SIM220 pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: BoolOp::And, values, range: _, }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::And, + values, + range: _, + }) = expr + else { return; }; if values.len() < 2 { @@ -594,7 +621,12 @@ pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { /// SIM221 pub(crate) fn expr_or_not_expr(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; }; if values.len() < 2 { @@ -672,7 +704,12 @@ fn is_short_circuit( expected_op: BoolOp, checker: &Checker, ) -> Option<(Edit, ContentAround)> { - let Expr::BoolOp(ast::ExprBoolOp { op, values, range: _, }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op, + values, + range: _, + }) = expr + else { return None; }; if *op != expected_op { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs index d1a570a0f2..bf9409f4bd 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs @@ -109,7 +109,11 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex let Some(arg) = args.get(0) else { return; }; - let Expr::Constant(ast::ExprConstant { value: Constant::Str(env_var), .. }) = arg else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Str(env_var), + .. + }) = arg + else { return; }; if !checker @@ -143,7 +147,12 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr else { return; }; - let Expr::Attribute(ast::ExprAttribute { value: attr_value, attr, .. }) = value.as_ref() else { + let Expr::Attribute(ast::ExprAttribute { + value: attr_value, + attr, + .. + }) = value.as_ref() + else { return; }; let Expr::Name(ast::ExprName { id, .. }) = attr_value.as_ref() else { @@ -152,7 +161,12 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { if id != "os" || attr != "environ" { return; } - let Expr::Constant(ast::ExprConstant { value: Constant::Str(env_var), kind, range: _ }) = slice.as_ref() else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Str(env_var), + kind, + range: _, + }) = slice.as_ref() + else { return; }; let capital_env_var = env_var.to_ascii_uppercase(); @@ -184,13 +198,19 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { /// SIM910 pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) { - let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = expr else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + range: _, + }) = expr + else { return; }; if !keywords.is_empty() { return; } - let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func.as_ref() else { + let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else { return; }; if !value.is_dict_expr() { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs index 9466328880..0accafcc0f 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs @@ -300,7 +300,15 @@ fn is_main_check(expr: &Expr) -> bool { /// ... /// ``` fn find_last_nested_if(body: &[Stmt]) -> Option<(&Expr, &Stmt)> { - let [Stmt::If(ast::StmtIf { test, body: inner_body, orelse, .. })] = body else { return None }; + let [Stmt::If(ast::StmtIf { + test, + body: inner_body, + orelse, + .. + })] = body + else { + return None; + }; if !orelse.is_empty() { return None; } @@ -429,10 +437,19 @@ fn is_one_line_return_bool(stmts: &[Stmt]) -> Option { /// SIM103 pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ }) = stmt else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = stmt + else { return; }; - let (Some(if_return), Some(else_return)) = (is_one_line_return_bool(body), is_one_line_return_bool(orelse)) else { + let (Some(if_return), Some(else_return)) = ( + is_one_line_return_bool(body), + is_one_line_return_bool(orelse), + ) else { return; }; @@ -515,25 +532,41 @@ fn contains_call_path(expr: &Expr, target: &[&str], semantic: &SemanticModel) -> /// SIM108 pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ } )= stmt else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = stmt + else { return; }; if body.len() != 1 || orelse.len() != 1 { return; } - let Stmt::Assign(ast::StmtAssign { targets: body_targets, value: body_value, .. } )= &body[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: body_targets, + value: body_value, + .. + }) = &body[0] + else { return; }; - let Stmt::Assign(ast::StmtAssign { targets: orelse_targets, value: orelse_value, .. } )= &orelse[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: orelse_targets, + value: orelse_value, + .. + }) = &orelse[0] + else { return; }; if body_targets.len() != 1 || orelse_targets.len() != 1 { return; } - let Expr::Name(ast::ExprName { id: body_id, .. } )= &body_targets[0] else { + let Expr::Name(ast::ExprName { id: body_id, .. }) = &body_targets[0] else { return; }; - let Expr::Name(ast::ExprName { id: orelse_id, .. } )= &orelse_targets[0] else { + let Expr::Name(ast::ExprName { id: orelse_id, .. }) = &orelse_targets[0] else { return; }; if body_id != orelse_id { @@ -638,7 +671,13 @@ fn get_if_body_pairs<'a>( if orelse.len() != 1 { break; } - let Stmt::If(ast::StmtIf { test, body, orelse: orelse_orelse, range: _ }) = &orelse[0] else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse: orelse_orelse, + range: _, + }) = &orelse[0] + else { break; }; pairs.push((test, body)); @@ -649,7 +688,13 @@ fn get_if_body_pairs<'a>( /// SIM114 pub(crate) fn if_with_same_arms(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ }) = stmt else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = stmt + else { return; }; @@ -718,7 +763,8 @@ pub(crate) fn manual_dict_lookup( ops, comparators, range: _, - })= &test else { + }) = &test + else { return; }; let Expr::Name(ast::ExprName { id: target, .. }) = left.as_ref() else { @@ -736,7 +782,10 @@ pub(crate) fn manual_dict_lookup( if comparators.len() != 1 { return; } - let Expr::Constant(ast::ExprConstant { value: constant, .. }) = &comparators[0] else { + let Expr::Constant(ast::ExprConstant { + value: constant, .. + }) = &comparators[0] + else { return; }; let Stmt::Return(ast::StmtReturn { value, range: _ }) = &body[0] else { @@ -783,7 +832,13 @@ pub(crate) fn manual_dict_lookup( let mut child: Option<&Stmt> = orelse.get(0); while let Some(current) = child.take() { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ }) = ¤t else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = ¤t + else { return; }; if body.len() != 1 { @@ -796,8 +851,9 @@ pub(crate) fn manual_dict_lookup( left, ops, comparators, - range: _ - } )= test.as_ref() else { + range: _, + }) = test.as_ref() + else { return; }; let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else { @@ -809,10 +865,13 @@ pub(crate) fn manual_dict_lookup( if comparators.len() != 1 { return; } - let Expr::Constant(ast::ExprConstant { value: constant, .. } )= &comparators[0] else { + let Expr::Constant(ast::ExprConstant { + value: constant, .. + }) = &comparators[0] + else { return; }; - let Stmt::Return(ast::StmtReturn { value, range: _ } )= &body[0] else { + let Stmt::Return(ast::StmtReturn { value, range: _ }) = &body[0] else { return; }; if value.as_ref().map_or(false, |value| { @@ -859,19 +918,35 @@ pub(crate) fn use_dict_get_with_default( if body.len() != 1 || orelse.len() != 1 { return; } - let Stmt::Assign(ast::StmtAssign { targets: body_var, value: body_value, ..}) = &body[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: body_var, + value: body_value, + .. + }) = &body[0] + else { return; }; if body_var.len() != 1 { return; }; - let Stmt::Assign(ast::StmtAssign { targets: orelse_var, value: orelse_value, .. }) = &orelse[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: orelse_var, + value: orelse_value, + .. + }) = &orelse[0] + else { return; }; if orelse_var.len() != 1 { return; }; - let Expr::Compare(ast::ExprCompare { left: test_key, ops , comparators: test_dict, range: _ }) = &test else { + let Expr::Compare(ast::ExprCompare { + left: test_key, + ops, + comparators: test_dict, + range: _, + }) = &test + else { return; }; if test_dict.len() != 1 { @@ -885,7 +960,12 @@ pub(crate) fn use_dict_get_with_default( } }; let test_dict = &test_dict[0]; - let Expr::Subscript(ast::ExprSubscript { value: expected_subscript, slice: expected_slice, .. } ) = expected_value.as_ref() else { + let Expr::Subscript(ast::ExprSubscript { + value: expected_subscript, + slice: expected_slice, + .. + }) = expected_value.as_ref() + else { return; }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs index eee0b0d1d1..d9f15d068e 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -141,13 +141,13 @@ pub(crate) fn explicit_true_false_in_ifexpr( body: &Expr, orelse: &Expr, ) { - let Expr::Constant(ast::ExprConstant { value, .. } )= &body else { + let Expr::Constant(ast::ExprConstant { value, .. }) = &body else { return; }; if !matches!(value, Constant::Bool(true)) { return; } - let Expr::Constant(ast::ExprConstant { value, .. } )= &orelse else { + let Expr::Constant(ast::ExprConstant { value, .. }) = &orelse else { return; }; if !matches!(value, Constant::Bool(false)) { @@ -237,7 +237,12 @@ pub(crate) fn twisted_arms_in_ifexpr( body: &Expr, orelse: &Expr, ) { - let Expr::UnaryOp(ast::ExprUnaryOp { op, operand: test_operand, range: _ } )= &test else { + let Expr::UnaryOp(ast::ExprUnaryOp { + op, + operand: test_operand, + range: _, + }) = &test + else { return; }; if !op.is_not() { @@ -245,10 +250,10 @@ pub(crate) fn twisted_arms_in_ifexpr( } // Check if the test operand and else branch use the same variable. - let Expr::Name(ast::ExprName { id: test_id, .. } )= test_operand.as_ref() else { + let Expr::Name(ast::ExprName { id: test_id, .. }) = test_operand.as_ref() else { return; }; - let Expr::Name(ast::ExprName {id: orelse_id, ..}) = orelse else { + let Expr::Name(ast::ExprName { id: orelse_id, .. }) = orelse else { return; }; if !test_id.eq(orelse_id) { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs index 05b96f3e19..2febbca3a6 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs @@ -127,7 +127,13 @@ fn is_dunder_method(name: &str) -> bool { } fn is_exception_check(stmt: &Stmt) -> bool { - let Stmt::If(ast::StmtIf {test: _, body, orelse: _, range: _ })= stmt else { + let Stmt::If(ast::StmtIf { + test: _, + body, + orelse: _, + range: _, + }) = stmt + else { return false; }; if body.len() != 1 { @@ -149,7 +155,13 @@ pub(crate) fn negation_with_equal_op( if !matches!(op, UnaryOp::Not) { return; } - let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _}) = operand else { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = operand + else { return; }; if !matches!(&ops[..], [CmpOp::Eq]) { @@ -201,7 +213,13 @@ pub(crate) fn negation_with_not_equal_op( if !matches!(op, UnaryOp::Not) { return; } - let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _}) = operand else { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = operand + else { return; }; if !matches!(&ops[..], [CmpOp::NotEq]) { @@ -248,7 +266,12 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: UnaryOp, o if !matches!(op, UnaryOp::Not) { return; } - let Expr::UnaryOp(ast::ExprUnaryOp { op: operand_op, operand, range: _ }) = operand else { + let Expr::UnaryOp(ast::ExprUnaryOp { + op: operand_op, + operand, + range: _, + }) = operand + else { return; }; if !matches!(operand_op, UnaryOp::Not) { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs index 0988aaf3b3..3199c6c509 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs @@ -90,7 +90,8 @@ pub(crate) fn fix_nested_if_statements( body: Suite::IndentedBlock(ref mut outer_body), orelse: None, .. - } = outer_if else { + } = outer_if + else { bail!("Expected outer if to have indented body and no else") }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs index b3636cabbc..649496bb8b 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs @@ -54,13 +54,12 @@ pub(crate) fn fix_multiple_with_statements( let With { body: Suite::IndentedBlock(ref mut outer_body), .. - } = outer_with else { + } = outer_with + else { bail!("Expected outer with to have indented body") }; - let [Statement::Compound(CompoundStatement::With(inner_with))] = - &mut *outer_body.body - else { + let [Statement::Compound(CompoundStatement::With(inner_with))] = &mut *outer_body.body else { bail!("Expected one inner with statement"); }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs index 7bdd4b2365..f4738c06bc 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs @@ -71,8 +71,9 @@ fn key_in_dict(checker: &mut Checker, left: &Expr, right: &Expr, range: TextRang func, args, keywords, - range: _ - }) = &right else { + range: _, + }) = &right + else { return; }; if !(args.is_empty() && keywords.is_empty()) { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs b/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs index 98c35c4d15..090d9c3e1d 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs @@ -49,9 +49,9 @@ fn match_async_exit_stack(semantic: &SemanticModel) -> bool { let Expr::Await(ast::ExprAwait { value, range: _ }) = expr else { return false; }; - let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { - return false; - }; + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { + return false; + }; let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { return false; }; @@ -80,9 +80,9 @@ fn match_exit_stack(semantic: &SemanticModel) -> bool { let Some(expr) = semantic.expr_parent() else { return false; }; - let Expr::Call(ast::ExprCall { func, .. }) = expr else { - return false; - }; + let Expr::Call(ast::ExprCall { func, .. }) = expr else { + return false; + }; let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { return false; }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs index 340ad9f956..f0e757b111 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -221,7 +221,8 @@ fn return_values_for_else(stmt: &Stmt) -> Option { iter, orelse, .. - }) = stmt else { + }) = stmt + else { return None; }; @@ -236,8 +237,10 @@ fn return_values_for_else(stmt: &Stmt) -> Option { let Stmt::If(ast::StmtIf { body: nested_body, test: nested_test, - orelse: nested_orelse, range: _, - }) = &body[0] else { + orelse: nested_orelse, + range: _, + }) = &body[0] + else { return None; }; if nested_body.len() != 1 { @@ -252,18 +255,30 @@ fn return_values_for_else(stmt: &Stmt) -> Option { let Some(value) = value else { return None; }; - let Expr::Constant(ast::ExprConstant { value: Constant::Bool(value), .. }) = value.as_ref() else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Bool(value), + .. + }) = value.as_ref() + else { return None; }; // The `else` block has to contain a single `return True` or `return False`. - let Stmt::Return(ast::StmtReturn { value: next_value, range: _ }) = &orelse[0] else { + let Stmt::Return(ast::StmtReturn { + value: next_value, + range: _, + }) = &orelse[0] + else { return None; }; let Some(next_value) = next_value else { return None; }; - let Expr::Constant(ast::ExprConstant { value: Constant::Bool(next_value), .. }) = next_value.as_ref() else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Bool(next_value), + .. + }) = next_value.as_ref() + else { return None; }; @@ -286,7 +301,8 @@ fn return_values_for_siblings<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option(stmt: &'a Stmt, sibling: &'a Stmt) -> Option(stmt: &'a Stmt, sibling: &'a Stmt) -> Option Option { } pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner: &str) { - let Expr::Call(ast::ExprCall { - args, - keywords, - .. - }) = expr else { + let Expr::Call(ast::ExprCall { args, keywords, .. }) = expr else { return; }; @@ -111,7 +107,9 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner: // Try to build the fstring (internally checks whether e.g. the elements are // convertible to f-string parts). - let Some(new_expr) = build_fstring(joiner, joinees) else { return }; + let Some(new_expr) = build_fstring(joiner, joinees) else { + return; + }; let contents = checker.generator().expr(&new_expr); diff --git a/crates/ruff/src/rules/isort/rules/add_required_imports.rs b/crates/ruff/src/rules/isort/rules/add_required_imports.rs index 8542b4ea9a..9abcb3971c 100644 --- a/crates/ruff/src/rules/isort/rules/add_required_imports.rs +++ b/crates/ruff/src/rules/isort/rules/add_required_imports.rs @@ -56,10 +56,7 @@ impl AlwaysAutofixableViolation for MissingRequiredImport { fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool { match target { AnyImport::Import(target) => { - let Stmt::Import(ast::StmtImport { - names, - range: _, - }) = &stmt else { + let Stmt::Import(ast::StmtImport { names, range: _ }) = &stmt else { return false; }; names.iter().any(|alias| { @@ -71,8 +68,9 @@ fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool { module, names, level, - range: _, - }) = &stmt else { + range: _, + }) = &stmt + else { return false; }; module.as_deref() == target.module diff --git a/crates/ruff/src/rules/numpy/mod.rs b/crates/ruff/src/rules/numpy/mod.rs index 37ddc17ffb..2bdb951dac 100644 --- a/crates/ruff/src/rules/numpy/mod.rs +++ b/crates/ruff/src/rules/numpy/mod.rs @@ -15,6 +15,7 @@ mod tests { #[test_case(Rule::NumpyDeprecatedTypeAlias, Path::new("NPY001.py"))] #[test_case(Rule::NumpyLegacyRandom, Path::new("NPY002.py"))] + #[test_case(Rule::NumpyDeprecatedFunction, Path::new("NPY003.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/numpy/rules/deprecated_function.rs b/crates/ruff/src/rules/numpy/rules/deprecated_function.rs new file mode 100644 index 0000000000..3fb2774cfb --- /dev/null +++ b/crates/ruff/src/rules/numpy/rules/deprecated_function.rs @@ -0,0 +1,97 @@ +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for uses of deprecated NumPy functions. +/// +/// ## Why is this bad? +/// When NumPy functions are deprecated, they are usually replaced with +/// newer, more efficient versions, or with functions that are more +/// consistent with the rest of the NumPy API. +/// +/// Prefer newer APIs over deprecated ones. +/// +/// ## Examples +/// ```python +/// import numpy as np +/// +/// np.alltrue([True, False]) +/// ``` +/// +/// Use instead: +/// ```python +/// import numpy as np +/// +/// np.all([True, False]) +/// ``` +#[violation] +pub struct NumpyDeprecatedFunction { + existing: String, + replacement: String, +} + +impl Violation for NumpyDeprecatedFunction { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let NumpyDeprecatedFunction { + existing, + replacement, + } = self; + format!("`np.{existing}` is deprecated; use `np.{replacement}` instead") + } + + fn autofix_title(&self) -> Option { + let NumpyDeprecatedFunction { replacement, .. } = self; + Some(format!("Replace with `np.{replacement}`")) + } +} + +/// NPY003 +pub(crate) fn deprecated_function(checker: &mut Checker, expr: &Expr) { + if let Some((existing, replacement)) = + checker + .semantic() + .resolve_call_path(expr) + .and_then(|call_path| match call_path.as_slice() { + ["numpy", "round_"] => Some(("round_", "round")), + ["numpy", "product"] => Some(("product", "prod")), + ["numpy", "cumproduct"] => Some(("cumproduct", "cumprod")), + ["numpy", "sometrue"] => Some(("sometrue", "any")), + ["numpy", "alltrue"] => Some(("alltrue", "all")), + _ => None, + }) + { + let mut diagnostic = Diagnostic::new( + NumpyDeprecatedFunction { + existing: existing.to_string(), + replacement: replacement.to_string(), + }, + expr.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + match expr { + Expr::Name(_) => { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + replacement.to_string(), + expr.range(), + ))); + } + Expr::Attribute(ast::ExprAttribute { attr, .. }) => { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + replacement.to_string(), + attr.range(), + ))); + } + _ => {} + } + } + checker.diagnostics.push(diagnostic); + } +} diff --git a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs b/crates/ruff/src/rules/numpy/rules/legacy_random.rs similarity index 98% rename from crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs rename to crates/ruff/src/rules/numpy/rules/legacy_random.rs index 46b66a65e4..cdcd68f149 100644 --- a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs +++ b/crates/ruff/src/rules/numpy/rules/legacy_random.rs @@ -57,7 +57,7 @@ impl Violation for NumpyLegacyRandom { } /// NPY002 -pub(crate) fn numpy_legacy_random(checker: &mut Checker, expr: &Expr) { +pub(crate) fn legacy_random(checker: &mut Checker, expr: &Expr) { if let Some(method_name) = checker .semantic() .resolve_call_path(expr) diff --git a/crates/ruff/src/rules/numpy/rules/mod.rs b/crates/ruff/src/rules/numpy/rules/mod.rs index 25c3f2fb17..7c46515e76 100644 --- a/crates/ruff/src/rules/numpy/rules/mod.rs +++ b/crates/ruff/src/rules/numpy/rules/mod.rs @@ -1,5 +1,7 @@ +pub(crate) use deprecated_function::*; pub(crate) use deprecated_type_alias::*; -pub(crate) use numpy_legacy_random::*; +pub(crate) use legacy_random::*; +mod deprecated_function; mod deprecated_type_alias; -mod numpy_legacy_random; +mod legacy_random; diff --git a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap new file mode 100644 index 0000000000..1165e5f488 --- /dev/null +++ b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap @@ -0,0 +1,201 @@ +--- +source: crates/ruff/src/rules/numpy/mod.rs +--- +NPY003.py:3:1: NPY003 [*] `np.round_` is deprecated; use `np.round` instead + | +1 | import numpy as np +2 | +3 | np.round_(np.random.rand(5, 5), 2) + | ^^^^^^^^^ NPY003 +4 | np.product(np.random.rand(5, 5)) +5 | np.cumproduct(np.random.rand(5, 5)) + | + = help: Replace with `np.round` + +ℹ Suggested fix +1 1 | import numpy as np +2 2 | +3 |-np.round_(np.random.rand(5, 5), 2) + 3 |+np.round(np.random.rand(5, 5), 2) +4 4 | np.product(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) + +NPY003.py:4:1: NPY003 [*] `np.product` is deprecated; use `np.prod` instead + | +3 | np.round_(np.random.rand(5, 5), 2) +4 | np.product(np.random.rand(5, 5)) + | ^^^^^^^^^^ NPY003 +5 | np.cumproduct(np.random.rand(5, 5)) +6 | np.sometrue(np.random.rand(5, 5)) + | + = help: Replace with `np.prod` + +ℹ Suggested fix +1 1 | import numpy as np +2 2 | +3 3 | np.round_(np.random.rand(5, 5), 2) +4 |-np.product(np.random.rand(5, 5)) + 4 |+np.prod(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) +7 7 | np.alltrue(np.random.rand(5, 5)) + +NPY003.py:5:1: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instead + | +3 | np.round_(np.random.rand(5, 5), 2) +4 | np.product(np.random.rand(5, 5)) +5 | np.cumproduct(np.random.rand(5, 5)) + | ^^^^^^^^^^^^^ NPY003 +6 | np.sometrue(np.random.rand(5, 5)) +7 | np.alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.cumprod` + +ℹ Suggested fix +2 2 | +3 3 | np.round_(np.random.rand(5, 5), 2) +4 4 | np.product(np.random.rand(5, 5)) +5 |-np.cumproduct(np.random.rand(5, 5)) + 5 |+np.cumprod(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) +7 7 | np.alltrue(np.random.rand(5, 5)) +8 8 | + +NPY003.py:6:1: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead + | +4 | np.product(np.random.rand(5, 5)) +5 | np.cumproduct(np.random.rand(5, 5)) +6 | np.sometrue(np.random.rand(5, 5)) + | ^^^^^^^^^^^ NPY003 +7 | np.alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.any` + +ℹ Suggested fix +3 3 | np.round_(np.random.rand(5, 5), 2) +4 4 | np.product(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 |-np.sometrue(np.random.rand(5, 5)) + 6 |+np.any(np.random.rand(5, 5)) +7 7 | np.alltrue(np.random.rand(5, 5)) +8 8 | +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue + +NPY003.py:7:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead + | +5 | np.cumproduct(np.random.rand(5, 5)) +6 | np.sometrue(np.random.rand(5, 5)) +7 | np.alltrue(np.random.rand(5, 5)) + | ^^^^^^^^^^ NPY003 +8 | +9 | from numpy import round_, product, cumproduct, sometrue, alltrue + | + = help: Replace with `np.all` + +ℹ Suggested fix +4 4 | np.product(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) +7 |-np.alltrue(np.random.rand(5, 5)) + 7 |+np.all(np.random.rand(5, 5)) +8 8 | +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 10 | + +NPY003.py:11:1: NPY003 [*] `np.round_` is deprecated; use `np.round` instead + | + 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 | +11 | round_(np.random.rand(5, 5), 2) + | ^^^^^^ NPY003 +12 | product(np.random.rand(5, 5)) +13 | cumproduct(np.random.rand(5, 5)) + | + = help: Replace with `np.round` + +ℹ Suggested fix +8 8 | +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 10 | +11 |-round_(np.random.rand(5, 5), 2) + 11 |+round(np.random.rand(5, 5), 2) +12 12 | product(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) + +NPY003.py:12:1: NPY003 [*] `np.product` is deprecated; use `np.prod` instead + | +11 | round_(np.random.rand(5, 5), 2) +12 | product(np.random.rand(5, 5)) + | ^^^^^^^ NPY003 +13 | cumproduct(np.random.rand(5, 5)) +14 | sometrue(np.random.rand(5, 5)) + | + = help: Replace with `np.prod` + +ℹ Suggested fix +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 10 | +11 11 | round_(np.random.rand(5, 5), 2) +12 |-product(np.random.rand(5, 5)) + 12 |+prod(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) +15 15 | alltrue(np.random.rand(5, 5)) + +NPY003.py:13:1: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instead + | +11 | round_(np.random.rand(5, 5), 2) +12 | product(np.random.rand(5, 5)) +13 | cumproduct(np.random.rand(5, 5)) + | ^^^^^^^^^^ NPY003 +14 | sometrue(np.random.rand(5, 5)) +15 | alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.cumprod` + +ℹ Suggested fix +10 10 | +11 11 | round_(np.random.rand(5, 5), 2) +12 12 | product(np.random.rand(5, 5)) +13 |-cumproduct(np.random.rand(5, 5)) + 13 |+cumprod(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) +15 15 | alltrue(np.random.rand(5, 5)) + +NPY003.py:14:1: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead + | +12 | product(np.random.rand(5, 5)) +13 | cumproduct(np.random.rand(5, 5)) +14 | sometrue(np.random.rand(5, 5)) + | ^^^^^^^^ NPY003 +15 | alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.any` + +ℹ Suggested fix +11 11 | round_(np.random.rand(5, 5), 2) +12 12 | product(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 |-sometrue(np.random.rand(5, 5)) + 14 |+any(np.random.rand(5, 5)) +15 15 | alltrue(np.random.rand(5, 5)) + +NPY003.py:15:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead + | +13 | cumproduct(np.random.rand(5, 5)) +14 | sometrue(np.random.rand(5, 5)) +15 | alltrue(np.random.rand(5, 5)) + | ^^^^^^^ NPY003 + | + = help: Replace with `np.all` + +ℹ Suggested fix +12 12 | product(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) +15 |-alltrue(np.random.rand(5, 5)) + 15 |+all(np.random.rand(5, 5)) + + diff --git a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap index 38890264a4..c06c12bee6 100644 --- a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap +++ b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap @@ -1,498 +1,497 @@ --- source: crates/ruff/src/rules/numpy/mod.rs --- -NPY002.py:10:8: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` +NPY002.py:12:8: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` | - 8 | # instead of this (legacy version) - 9 | from numpy import random -10 | vals = random.standard_normal(10) +10 | from numpy import random +11 | +12 | vals = random.standard_normal(10) | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -11 | more_vals = random.standard_normal(10) -12 | numbers = random.integers(high, size=5) +13 | more_vals = random.standard_normal(10) +14 | numbers = random.integers(high, size=5) | -NPY002.py:11:13: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` +NPY002.py:13:13: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` | - 9 | from numpy import random -10 | vals = random.standard_normal(10) -11 | more_vals = random.standard_normal(10) +12 | vals = random.standard_normal(10) +13 | more_vals = random.standard_normal(10) | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -12 | numbers = random.integers(high, size=5) +14 | numbers = random.integers(high, size=5) | -NPY002.py:15:1: NPY002 Replace legacy `np.random.seed` call with `np.random.Generator` +NPY002.py:18:1: NPY002 Replace legacy `np.random.seed` call with `np.random.Generator` | -14 | import numpy -15 | numpy.random.seed() +16 | import numpy +17 | +18 | numpy.random.seed() | ^^^^^^^^^^^^^^^^^ NPY002 -16 | numpy.random.get_state() -17 | numpy.random.set_state() +19 | numpy.random.get_state() +20 | numpy.random.set_state() | -NPY002.py:16:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` +NPY002.py:19:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` | -14 | import numpy -15 | numpy.random.seed() -16 | numpy.random.get_state() +18 | numpy.random.seed() +19 | numpy.random.get_state() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -17 | numpy.random.set_state() -18 | numpy.random.rand() +20 | numpy.random.set_state() +21 | numpy.random.rand() | -NPY002.py:17:1: NPY002 Replace legacy `np.random.set_state` call with `np.random.Generator` +NPY002.py:20:1: NPY002 Replace legacy `np.random.set_state` call with `np.random.Generator` | -15 | numpy.random.seed() -16 | numpy.random.get_state() -17 | numpy.random.set_state() +18 | numpy.random.seed() +19 | numpy.random.get_state() +20 | numpy.random.set_state() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -18 | numpy.random.rand() -19 | numpy.random.randn() +21 | numpy.random.rand() +22 | numpy.random.randn() | -NPY002.py:18:1: NPY002 Replace legacy `np.random.rand` call with `np.random.Generator` +NPY002.py:21:1: NPY002 Replace legacy `np.random.rand` call with `np.random.Generator` | -16 | numpy.random.get_state() -17 | numpy.random.set_state() -18 | numpy.random.rand() +19 | numpy.random.get_state() +20 | numpy.random.set_state() +21 | numpy.random.rand() | ^^^^^^^^^^^^^^^^^ NPY002 -19 | numpy.random.randn() -20 | numpy.random.randint() +22 | numpy.random.randn() +23 | numpy.random.randint() | -NPY002.py:19:1: NPY002 Replace legacy `np.random.randn` call with `np.random.Generator` +NPY002.py:22:1: NPY002 Replace legacy `np.random.randn` call with `np.random.Generator` | -17 | numpy.random.set_state() -18 | numpy.random.rand() -19 | numpy.random.randn() +20 | numpy.random.set_state() +21 | numpy.random.rand() +22 | numpy.random.randn() | ^^^^^^^^^^^^^^^^^^ NPY002 -20 | numpy.random.randint() -21 | numpy.random.random_integers() +23 | numpy.random.randint() +24 | numpy.random.random_integers() | -NPY002.py:20:1: NPY002 Replace legacy `np.random.randint` call with `np.random.Generator` +NPY002.py:23:1: NPY002 Replace legacy `np.random.randint` call with `np.random.Generator` | -18 | numpy.random.rand() -19 | numpy.random.randn() -20 | numpy.random.randint() +21 | numpy.random.rand() +22 | numpy.random.randn() +23 | numpy.random.randint() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -21 | numpy.random.random_integers() -22 | numpy.random.random_sample() +24 | numpy.random.random_integers() +25 | numpy.random.random_sample() | -NPY002.py:21:1: NPY002 Replace legacy `np.random.random_integers` call with `np.random.Generator` +NPY002.py:24:1: NPY002 Replace legacy `np.random.random_integers` call with `np.random.Generator` | -19 | numpy.random.randn() -20 | numpy.random.randint() -21 | numpy.random.random_integers() +22 | numpy.random.randn() +23 | numpy.random.randint() +24 | numpy.random.random_integers() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -22 | numpy.random.random_sample() -23 | numpy.random.choice() +25 | numpy.random.random_sample() +26 | numpy.random.choice() | -NPY002.py:22:1: NPY002 Replace legacy `np.random.random_sample` call with `np.random.Generator` +NPY002.py:25:1: NPY002 Replace legacy `np.random.random_sample` call with `np.random.Generator` | -20 | numpy.random.randint() -21 | numpy.random.random_integers() -22 | numpy.random.random_sample() +23 | numpy.random.randint() +24 | numpy.random.random_integers() +25 | numpy.random.random_sample() | ^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -23 | numpy.random.choice() -24 | numpy.random.bytes() +26 | numpy.random.choice() +27 | numpy.random.bytes() | -NPY002.py:23:1: NPY002 Replace legacy `np.random.choice` call with `np.random.Generator` +NPY002.py:26:1: NPY002 Replace legacy `np.random.choice` call with `np.random.Generator` | -21 | numpy.random.random_integers() -22 | numpy.random.random_sample() -23 | numpy.random.choice() +24 | numpy.random.random_integers() +25 | numpy.random.random_sample() +26 | numpy.random.choice() | ^^^^^^^^^^^^^^^^^^^ NPY002 -24 | numpy.random.bytes() -25 | numpy.random.shuffle() +27 | numpy.random.bytes() +28 | numpy.random.shuffle() | -NPY002.py:24:1: NPY002 Replace legacy `np.random.bytes` call with `np.random.Generator` +NPY002.py:27:1: NPY002 Replace legacy `np.random.bytes` call with `np.random.Generator` | -22 | numpy.random.random_sample() -23 | numpy.random.choice() -24 | numpy.random.bytes() +25 | numpy.random.random_sample() +26 | numpy.random.choice() +27 | numpy.random.bytes() | ^^^^^^^^^^^^^^^^^^ NPY002 -25 | numpy.random.shuffle() -26 | numpy.random.permutation() +28 | numpy.random.shuffle() +29 | numpy.random.permutation() | -NPY002.py:25:1: NPY002 Replace legacy `np.random.shuffle` call with `np.random.Generator` +NPY002.py:28:1: NPY002 Replace legacy `np.random.shuffle` call with `np.random.Generator` | -23 | numpy.random.choice() -24 | numpy.random.bytes() -25 | numpy.random.shuffle() +26 | numpy.random.choice() +27 | numpy.random.bytes() +28 | numpy.random.shuffle() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -26 | numpy.random.permutation() -27 | numpy.random.beta() +29 | numpy.random.permutation() +30 | numpy.random.beta() | -NPY002.py:26:1: NPY002 Replace legacy `np.random.permutation` call with `np.random.Generator` +NPY002.py:29:1: NPY002 Replace legacy `np.random.permutation` call with `np.random.Generator` | -24 | numpy.random.bytes() -25 | numpy.random.shuffle() -26 | numpy.random.permutation() +27 | numpy.random.bytes() +28 | numpy.random.shuffle() +29 | numpy.random.permutation() | ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -27 | numpy.random.beta() -28 | numpy.random.binomial() +30 | numpy.random.beta() +31 | numpy.random.binomial() | -NPY002.py:27:1: NPY002 Replace legacy `np.random.beta` call with `np.random.Generator` +NPY002.py:30:1: NPY002 Replace legacy `np.random.beta` call with `np.random.Generator` | -25 | numpy.random.shuffle() -26 | numpy.random.permutation() -27 | numpy.random.beta() +28 | numpy.random.shuffle() +29 | numpy.random.permutation() +30 | numpy.random.beta() | ^^^^^^^^^^^^^^^^^ NPY002 -28 | numpy.random.binomial() -29 | numpy.random.chisquare() +31 | numpy.random.binomial() +32 | numpy.random.chisquare() | -NPY002.py:28:1: NPY002 Replace legacy `np.random.binomial` call with `np.random.Generator` +NPY002.py:31:1: NPY002 Replace legacy `np.random.binomial` call with `np.random.Generator` | -26 | numpy.random.permutation() -27 | numpy.random.beta() -28 | numpy.random.binomial() +29 | numpy.random.permutation() +30 | numpy.random.beta() +31 | numpy.random.binomial() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -29 | numpy.random.chisquare() -30 | numpy.random.dirichlet() +32 | numpy.random.chisquare() +33 | numpy.random.dirichlet() | -NPY002.py:29:1: NPY002 Replace legacy `np.random.chisquare` call with `np.random.Generator` +NPY002.py:32:1: NPY002 Replace legacy `np.random.chisquare` call with `np.random.Generator` | -27 | numpy.random.beta() -28 | numpy.random.binomial() -29 | numpy.random.chisquare() +30 | numpy.random.beta() +31 | numpy.random.binomial() +32 | numpy.random.chisquare() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -30 | numpy.random.dirichlet() -31 | numpy.random.exponential() +33 | numpy.random.dirichlet() +34 | numpy.random.exponential() | -NPY002.py:30:1: NPY002 Replace legacy `np.random.dirichlet` call with `np.random.Generator` +NPY002.py:33:1: NPY002 Replace legacy `np.random.dirichlet` call with `np.random.Generator` | -28 | numpy.random.binomial() -29 | numpy.random.chisquare() -30 | numpy.random.dirichlet() +31 | numpy.random.binomial() +32 | numpy.random.chisquare() +33 | numpy.random.dirichlet() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -31 | numpy.random.exponential() -32 | numpy.random.f() +34 | numpy.random.exponential() +35 | numpy.random.f() | -NPY002.py:31:1: NPY002 Replace legacy `np.random.exponential` call with `np.random.Generator` +NPY002.py:34:1: NPY002 Replace legacy `np.random.exponential` call with `np.random.Generator` | -29 | numpy.random.chisquare() -30 | numpy.random.dirichlet() -31 | numpy.random.exponential() +32 | numpy.random.chisquare() +33 | numpy.random.dirichlet() +34 | numpy.random.exponential() | ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -32 | numpy.random.f() -33 | numpy.random.gamma() +35 | numpy.random.f() +36 | numpy.random.gamma() | -NPY002.py:32:1: NPY002 Replace legacy `np.random.f` call with `np.random.Generator` +NPY002.py:35:1: NPY002 Replace legacy `np.random.f` call with `np.random.Generator` | -30 | numpy.random.dirichlet() -31 | numpy.random.exponential() -32 | numpy.random.f() +33 | numpy.random.dirichlet() +34 | numpy.random.exponential() +35 | numpy.random.f() | ^^^^^^^^^^^^^^ NPY002 -33 | numpy.random.gamma() -34 | numpy.random.geometric() +36 | numpy.random.gamma() +37 | numpy.random.geometric() | -NPY002.py:33:1: NPY002 Replace legacy `np.random.gamma` call with `np.random.Generator` +NPY002.py:36:1: NPY002 Replace legacy `np.random.gamma` call with `np.random.Generator` | -31 | numpy.random.exponential() -32 | numpy.random.f() -33 | numpy.random.gamma() +34 | numpy.random.exponential() +35 | numpy.random.f() +36 | numpy.random.gamma() | ^^^^^^^^^^^^^^^^^^ NPY002 -34 | numpy.random.geometric() -35 | numpy.random.get_state() +37 | numpy.random.geometric() +38 | numpy.random.get_state() | -NPY002.py:34:1: NPY002 Replace legacy `np.random.geometric` call with `np.random.Generator` +NPY002.py:37:1: NPY002 Replace legacy `np.random.geometric` call with `np.random.Generator` | -32 | numpy.random.f() -33 | numpy.random.gamma() -34 | numpy.random.geometric() +35 | numpy.random.f() +36 | numpy.random.gamma() +37 | numpy.random.geometric() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -35 | numpy.random.get_state() -36 | numpy.random.gumbel() +38 | numpy.random.get_state() +39 | numpy.random.gumbel() | -NPY002.py:35:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` +NPY002.py:38:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` | -33 | numpy.random.gamma() -34 | numpy.random.geometric() -35 | numpy.random.get_state() +36 | numpy.random.gamma() +37 | numpy.random.geometric() +38 | numpy.random.get_state() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -36 | numpy.random.gumbel() -37 | numpy.random.hypergeometric() +39 | numpy.random.gumbel() +40 | numpy.random.hypergeometric() | -NPY002.py:36:1: NPY002 Replace legacy `np.random.gumbel` call with `np.random.Generator` +NPY002.py:39:1: NPY002 Replace legacy `np.random.gumbel` call with `np.random.Generator` | -34 | numpy.random.geometric() -35 | numpy.random.get_state() -36 | numpy.random.gumbel() +37 | numpy.random.geometric() +38 | numpy.random.get_state() +39 | numpy.random.gumbel() | ^^^^^^^^^^^^^^^^^^^ NPY002 -37 | numpy.random.hypergeometric() -38 | numpy.random.laplace() +40 | numpy.random.hypergeometric() +41 | numpy.random.laplace() | -NPY002.py:37:1: NPY002 Replace legacy `np.random.hypergeometric` call with `np.random.Generator` +NPY002.py:40:1: NPY002 Replace legacy `np.random.hypergeometric` call with `np.random.Generator` | -35 | numpy.random.get_state() -36 | numpy.random.gumbel() -37 | numpy.random.hypergeometric() +38 | numpy.random.get_state() +39 | numpy.random.gumbel() +40 | numpy.random.hypergeometric() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -38 | numpy.random.laplace() -39 | numpy.random.logistic() +41 | numpy.random.laplace() +42 | numpy.random.logistic() | -NPY002.py:38:1: NPY002 Replace legacy `np.random.laplace` call with `np.random.Generator` +NPY002.py:41:1: NPY002 Replace legacy `np.random.laplace` call with `np.random.Generator` | -36 | numpy.random.gumbel() -37 | numpy.random.hypergeometric() -38 | numpy.random.laplace() +39 | numpy.random.gumbel() +40 | numpy.random.hypergeometric() +41 | numpy.random.laplace() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -39 | numpy.random.logistic() -40 | numpy.random.lognormal() +42 | numpy.random.logistic() +43 | numpy.random.lognormal() | -NPY002.py:39:1: NPY002 Replace legacy `np.random.logistic` call with `np.random.Generator` +NPY002.py:42:1: NPY002 Replace legacy `np.random.logistic` call with `np.random.Generator` | -37 | numpy.random.hypergeometric() -38 | numpy.random.laplace() -39 | numpy.random.logistic() +40 | numpy.random.hypergeometric() +41 | numpy.random.laplace() +42 | numpy.random.logistic() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -40 | numpy.random.lognormal() -41 | numpy.random.logseries() +43 | numpy.random.lognormal() +44 | numpy.random.logseries() | -NPY002.py:40:1: NPY002 Replace legacy `np.random.lognormal` call with `np.random.Generator` +NPY002.py:43:1: NPY002 Replace legacy `np.random.lognormal` call with `np.random.Generator` | -38 | numpy.random.laplace() -39 | numpy.random.logistic() -40 | numpy.random.lognormal() +41 | numpy.random.laplace() +42 | numpy.random.logistic() +43 | numpy.random.lognormal() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -41 | numpy.random.logseries() -42 | numpy.random.multinomial() +44 | numpy.random.logseries() +45 | numpy.random.multinomial() | -NPY002.py:41:1: NPY002 Replace legacy `np.random.logseries` call with `np.random.Generator` +NPY002.py:44:1: NPY002 Replace legacy `np.random.logseries` call with `np.random.Generator` | -39 | numpy.random.logistic() -40 | numpy.random.lognormal() -41 | numpy.random.logseries() +42 | numpy.random.logistic() +43 | numpy.random.lognormal() +44 | numpy.random.logseries() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -42 | numpy.random.multinomial() -43 | numpy.random.multivariate_normal() +45 | numpy.random.multinomial() +46 | numpy.random.multivariate_normal() | -NPY002.py:42:1: NPY002 Replace legacy `np.random.multinomial` call with `np.random.Generator` +NPY002.py:45:1: NPY002 Replace legacy `np.random.multinomial` call with `np.random.Generator` | -40 | numpy.random.lognormal() -41 | numpy.random.logseries() -42 | numpy.random.multinomial() +43 | numpy.random.lognormal() +44 | numpy.random.logseries() +45 | numpy.random.multinomial() | ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -43 | numpy.random.multivariate_normal() -44 | numpy.random.negative_binomial() +46 | numpy.random.multivariate_normal() +47 | numpy.random.negative_binomial() | -NPY002.py:43:1: NPY002 Replace legacy `np.random.multivariate_normal` call with `np.random.Generator` +NPY002.py:46:1: NPY002 Replace legacy `np.random.multivariate_normal` call with `np.random.Generator` | -41 | numpy.random.logseries() -42 | numpy.random.multinomial() -43 | numpy.random.multivariate_normal() +44 | numpy.random.logseries() +45 | numpy.random.multinomial() +46 | numpy.random.multivariate_normal() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -44 | numpy.random.negative_binomial() -45 | numpy.random.noncentral_chisquare() +47 | numpy.random.negative_binomial() +48 | numpy.random.noncentral_chisquare() | -NPY002.py:44:1: NPY002 Replace legacy `np.random.negative_binomial` call with `np.random.Generator` +NPY002.py:47:1: NPY002 Replace legacy `np.random.negative_binomial` call with `np.random.Generator` | -42 | numpy.random.multinomial() -43 | numpy.random.multivariate_normal() -44 | numpy.random.negative_binomial() +45 | numpy.random.multinomial() +46 | numpy.random.multivariate_normal() +47 | numpy.random.negative_binomial() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -45 | numpy.random.noncentral_chisquare() -46 | numpy.random.noncentral_f() +48 | numpy.random.noncentral_chisquare() +49 | numpy.random.noncentral_f() | -NPY002.py:45:1: NPY002 Replace legacy `np.random.noncentral_chisquare` call with `np.random.Generator` +NPY002.py:48:1: NPY002 Replace legacy `np.random.noncentral_chisquare` call with `np.random.Generator` | -43 | numpy.random.multivariate_normal() -44 | numpy.random.negative_binomial() -45 | numpy.random.noncentral_chisquare() +46 | numpy.random.multivariate_normal() +47 | numpy.random.negative_binomial() +48 | numpy.random.noncentral_chisquare() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -46 | numpy.random.noncentral_f() -47 | numpy.random.normal() +49 | numpy.random.noncentral_f() +50 | numpy.random.normal() | -NPY002.py:46:1: NPY002 Replace legacy `np.random.noncentral_f` call with `np.random.Generator` +NPY002.py:49:1: NPY002 Replace legacy `np.random.noncentral_f` call with `np.random.Generator` | -44 | numpy.random.negative_binomial() -45 | numpy.random.noncentral_chisquare() -46 | numpy.random.noncentral_f() +47 | numpy.random.negative_binomial() +48 | numpy.random.noncentral_chisquare() +49 | numpy.random.noncentral_f() | ^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -47 | numpy.random.normal() -48 | numpy.random.pareto() +50 | numpy.random.normal() +51 | numpy.random.pareto() | -NPY002.py:47:1: NPY002 Replace legacy `np.random.normal` call with `np.random.Generator` +NPY002.py:50:1: NPY002 Replace legacy `np.random.normal` call with `np.random.Generator` | -45 | numpy.random.noncentral_chisquare() -46 | numpy.random.noncentral_f() -47 | numpy.random.normal() +48 | numpy.random.noncentral_chisquare() +49 | numpy.random.noncentral_f() +50 | numpy.random.normal() | ^^^^^^^^^^^^^^^^^^^ NPY002 -48 | numpy.random.pareto() -49 | numpy.random.poisson() +51 | numpy.random.pareto() +52 | numpy.random.poisson() | -NPY002.py:48:1: NPY002 Replace legacy `np.random.pareto` call with `np.random.Generator` +NPY002.py:51:1: NPY002 Replace legacy `np.random.pareto` call with `np.random.Generator` | -46 | numpy.random.noncentral_f() -47 | numpy.random.normal() -48 | numpy.random.pareto() +49 | numpy.random.noncentral_f() +50 | numpy.random.normal() +51 | numpy.random.pareto() | ^^^^^^^^^^^^^^^^^^^ NPY002 -49 | numpy.random.poisson() -50 | numpy.random.power() +52 | numpy.random.poisson() +53 | numpy.random.power() | -NPY002.py:49:1: NPY002 Replace legacy `np.random.poisson` call with `np.random.Generator` +NPY002.py:52:1: NPY002 Replace legacy `np.random.poisson` call with `np.random.Generator` | -47 | numpy.random.normal() -48 | numpy.random.pareto() -49 | numpy.random.poisson() +50 | numpy.random.normal() +51 | numpy.random.pareto() +52 | numpy.random.poisson() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -50 | numpy.random.power() -51 | numpy.random.rayleigh() +53 | numpy.random.power() +54 | numpy.random.rayleigh() | -NPY002.py:50:1: NPY002 Replace legacy `np.random.power` call with `np.random.Generator` +NPY002.py:53:1: NPY002 Replace legacy `np.random.power` call with `np.random.Generator` | -48 | numpy.random.pareto() -49 | numpy.random.poisson() -50 | numpy.random.power() +51 | numpy.random.pareto() +52 | numpy.random.poisson() +53 | numpy.random.power() | ^^^^^^^^^^^^^^^^^^ NPY002 -51 | numpy.random.rayleigh() -52 | numpy.random.standard_cauchy() +54 | numpy.random.rayleigh() +55 | numpy.random.standard_cauchy() | -NPY002.py:51:1: NPY002 Replace legacy `np.random.rayleigh` call with `np.random.Generator` +NPY002.py:54:1: NPY002 Replace legacy `np.random.rayleigh` call with `np.random.Generator` | -49 | numpy.random.poisson() -50 | numpy.random.power() -51 | numpy.random.rayleigh() +52 | numpy.random.poisson() +53 | numpy.random.power() +54 | numpy.random.rayleigh() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -52 | numpy.random.standard_cauchy() -53 | numpy.random.standard_exponential() +55 | numpy.random.standard_cauchy() +56 | numpy.random.standard_exponential() | -NPY002.py:52:1: NPY002 Replace legacy `np.random.standard_cauchy` call with `np.random.Generator` +NPY002.py:55:1: NPY002 Replace legacy `np.random.standard_cauchy` call with `np.random.Generator` | -50 | numpy.random.power() -51 | numpy.random.rayleigh() -52 | numpy.random.standard_cauchy() +53 | numpy.random.power() +54 | numpy.random.rayleigh() +55 | numpy.random.standard_cauchy() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -53 | numpy.random.standard_exponential() -54 | numpy.random.standard_gamma() +56 | numpy.random.standard_exponential() +57 | numpy.random.standard_gamma() | -NPY002.py:53:1: NPY002 Replace legacy `np.random.standard_exponential` call with `np.random.Generator` +NPY002.py:56:1: NPY002 Replace legacy `np.random.standard_exponential` call with `np.random.Generator` | -51 | numpy.random.rayleigh() -52 | numpy.random.standard_cauchy() -53 | numpy.random.standard_exponential() +54 | numpy.random.rayleigh() +55 | numpy.random.standard_cauchy() +56 | numpy.random.standard_exponential() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -54 | numpy.random.standard_gamma() -55 | numpy.random.standard_normal() +57 | numpy.random.standard_gamma() +58 | numpy.random.standard_normal() | -NPY002.py:54:1: NPY002 Replace legacy `np.random.standard_gamma` call with `np.random.Generator` +NPY002.py:57:1: NPY002 Replace legacy `np.random.standard_gamma` call with `np.random.Generator` | -52 | numpy.random.standard_cauchy() -53 | numpy.random.standard_exponential() -54 | numpy.random.standard_gamma() +55 | numpy.random.standard_cauchy() +56 | numpy.random.standard_exponential() +57 | numpy.random.standard_gamma() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -55 | numpy.random.standard_normal() -56 | numpy.random.standard_t() +58 | numpy.random.standard_normal() +59 | numpy.random.standard_t() | -NPY002.py:55:1: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` +NPY002.py:58:1: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` | -53 | numpy.random.standard_exponential() -54 | numpy.random.standard_gamma() -55 | numpy.random.standard_normal() +56 | numpy.random.standard_exponential() +57 | numpy.random.standard_gamma() +58 | numpy.random.standard_normal() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -56 | numpy.random.standard_t() -57 | numpy.random.triangular() +59 | numpy.random.standard_t() +60 | numpy.random.triangular() | -NPY002.py:56:1: NPY002 Replace legacy `np.random.standard_t` call with `np.random.Generator` +NPY002.py:59:1: NPY002 Replace legacy `np.random.standard_t` call with `np.random.Generator` | -54 | numpy.random.standard_gamma() -55 | numpy.random.standard_normal() -56 | numpy.random.standard_t() +57 | numpy.random.standard_gamma() +58 | numpy.random.standard_normal() +59 | numpy.random.standard_t() | ^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -57 | numpy.random.triangular() -58 | numpy.random.uniform() +60 | numpy.random.triangular() +61 | numpy.random.uniform() | -NPY002.py:57:1: NPY002 Replace legacy `np.random.triangular` call with `np.random.Generator` +NPY002.py:60:1: NPY002 Replace legacy `np.random.triangular` call with `np.random.Generator` | -55 | numpy.random.standard_normal() -56 | numpy.random.standard_t() -57 | numpy.random.triangular() +58 | numpy.random.standard_normal() +59 | numpy.random.standard_t() +60 | numpy.random.triangular() | ^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -58 | numpy.random.uniform() -59 | numpy.random.vonmises() +61 | numpy.random.uniform() +62 | numpy.random.vonmises() | -NPY002.py:58:1: NPY002 Replace legacy `np.random.uniform` call with `np.random.Generator` +NPY002.py:61:1: NPY002 Replace legacy `np.random.uniform` call with `np.random.Generator` | -56 | numpy.random.standard_t() -57 | numpy.random.triangular() -58 | numpy.random.uniform() +59 | numpy.random.standard_t() +60 | numpy.random.triangular() +61 | numpy.random.uniform() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -59 | numpy.random.vonmises() -60 | numpy.random.wald() +62 | numpy.random.vonmises() +63 | numpy.random.wald() | -NPY002.py:59:1: NPY002 Replace legacy `np.random.vonmises` call with `np.random.Generator` +NPY002.py:62:1: NPY002 Replace legacy `np.random.vonmises` call with `np.random.Generator` | -57 | numpy.random.triangular() -58 | numpy.random.uniform() -59 | numpy.random.vonmises() +60 | numpy.random.triangular() +61 | numpy.random.uniform() +62 | numpy.random.vonmises() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -60 | numpy.random.wald() -61 | numpy.random.weibull() +63 | numpy.random.wald() +64 | numpy.random.weibull() | -NPY002.py:60:1: NPY002 Replace legacy `np.random.wald` call with `np.random.Generator` +NPY002.py:63:1: NPY002 Replace legacy `np.random.wald` call with `np.random.Generator` | -58 | numpy.random.uniform() -59 | numpy.random.vonmises() -60 | numpy.random.wald() +61 | numpy.random.uniform() +62 | numpy.random.vonmises() +63 | numpy.random.wald() | ^^^^^^^^^^^^^^^^^ NPY002 -61 | numpy.random.weibull() -62 | numpy.random.zipf() +64 | numpy.random.weibull() +65 | numpy.random.zipf() | -NPY002.py:61:1: NPY002 Replace legacy `np.random.weibull` call with `np.random.Generator` +NPY002.py:64:1: NPY002 Replace legacy `np.random.weibull` call with `np.random.Generator` | -59 | numpy.random.vonmises() -60 | numpy.random.wald() -61 | numpy.random.weibull() +62 | numpy.random.vonmises() +63 | numpy.random.wald() +64 | numpy.random.weibull() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -62 | numpy.random.zipf() +65 | numpy.random.zipf() | -NPY002.py:62:1: NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator` +NPY002.py:65:1: NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator` | -60 | numpy.random.wald() -61 | numpy.random.weibull() -62 | numpy.random.zipf() +63 | numpy.random.wald() +64 | numpy.random.weibull() +65 | numpy.random.zipf() | ^^^^^^^^^^^^^^^^^ NPY002 | diff --git a/crates/ruff/src/rules/pandas_vet/fixes.rs b/crates/ruff/src/rules/pandas_vet/fixes.rs deleted file mode 100644 index 8a3d368f07..0000000000 --- a/crates/ruff/src/rules/pandas_vet/fixes.rs +++ /dev/null @@ -1,44 +0,0 @@ -use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; - -use ruff_diagnostics::{Edit, Fix}; -use ruff_python_ast::source_code::Locator; - -use crate::autofix::edits::remove_argument; - -fn match_name(expr: &Expr) -> Option<&str> { - if let Expr::Call(ast::ExprCall { func, .. }) = expr { - if let Expr::Attribute(ast::ExprAttribute { value, .. }) = func.as_ref() { - if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() { - return Some(id); - } - } - } - None -} - -/// Remove the `inplace` argument from a function call and replace it with an -/// assignment. -pub(super) fn convert_inplace_argument_to_assignment( - locator: &Locator, - expr: &Expr, - violation_range: TextRange, - args: &[Expr], - keywords: &[Keyword], -) -> Option { - // Add the assignment. - let name = match_name(expr)?; - let insert_assignment = Edit::insertion(format!("{name} = "), expr.start()); - - // Remove the `inplace` argument. - let remove_argument = remove_argument( - locator, - expr.start(), - violation_range, - args, - keywords, - false, - ) - .ok()?; - Some(Fix::suggested_edits(insert_assignment, [remove_argument])) -} diff --git a/crates/ruff/src/rules/pandas_vet/mod.rs b/crates/ruff/src/rules/pandas_vet/mod.rs index d3ac303cbc..295a83cd24 100644 --- a/crates/ruff/src/rules/pandas_vet/mod.rs +++ b/crates/ruff/src/rules/pandas_vet/mod.rs @@ -1,5 +1,4 @@ //! Rules from [pandas-vet](https://pypi.org/project/pandas-vet/). -pub(crate) mod fixes; pub(crate) mod helpers; pub(crate) mod rules; @@ -354,8 +353,10 @@ mod tests { "PD901_fail_df_var" )] fn contents(contents: &str, snapshot: &str) { - let diagnostics = - test_snippet(contents, &settings::Settings::for_rules(&Linter::PandasVet)); + let diagnostics = test_snippet( + contents, + &settings::Settings::for_rules(Linter::PandasVet.rules()), + ); assert_messages!(snapshot, diagnostics); } diff --git a/crates/ruff/src/rules/pandas_vet/rules/call.rs b/crates/ruff/src/rules/pandas_vet/rules/call.rs index be7dfadf24..a0ab67ca2c 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/call.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/call.rs @@ -62,7 +62,7 @@ impl Violation for PandasUseOfDotStack { pub(crate) fn call(checker: &mut Checker, func: &Expr) { let rules = &checker.settings.rules; - let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func else { + let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func else { return; }; let violation: DiagnosticKind = match attr.as_str() { diff --git a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs index cf5983213d..98a44a58d6 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -1,12 +1,15 @@ -use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{Expr, Keyword, Ranged}; -use ruff_diagnostics::{AutofixKind, Diagnostic, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_true; +use ruff_python_ast::source_code::Locator; use ruff_python_semantic::{BindingKind, Import}; +use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; use crate::registry::AsRule; -use crate::rules::pandas_vet::fixes::convert_inplace_argument_to_assignment; /// ## What it does /// Checks for `inplace=True` usages in `pandas` function and method @@ -50,23 +53,17 @@ impl Violation for PandasUseOfInplaceArgument { /// PD002 pub(crate) fn inplace_argument( - checker: &Checker, + checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr], keywords: &[Keyword], -) -> Option { - let mut seen_star = false; - let mut is_checkable = false; - let mut is_pandas = false; - +) { + // If the function was imported from another module, and it's _not_ Pandas, abort. if let Some(call_path) = checker.semantic().resolve_call_path(func) { - is_checkable = true; - - let module = call_path[0]; - is_pandas = checker - .semantic() - .find_binding(module) + if !call_path + .first() + .and_then(|module| checker.semantic().find_binding(module)) .map_or(false, |binding| { matches!( binding.kind, @@ -74,23 +71,20 @@ pub(crate) fn inplace_argument( qualified_name: "pandas" }) ) - }); + }) + { + return; + } } + let mut seen_star = false; for keyword in keywords.iter().rev() { let Some(arg) = &keyword.arg else { seen_star = true; continue; }; if arg == "inplace" { - let is_true_literal = match &keyword.value { - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(boolean), - .. - }) => *boolean, - _ => false, - }; - if is_true_literal { + if is_const_true(&keyword.value) { let mut diagnostic = Diagnostic::new(PandasUseOfInplaceArgument, keyword.range()); if checker.patch(diagnostic.kind.rule()) { // Avoid applying the fix if: @@ -110,7 +104,7 @@ pub(crate) fn inplace_argument( if let Some(fix) = convert_inplace_argument_to_assignment( checker.locator, expr, - diagnostic.range(), + keyword.range(), args, keywords, ) { @@ -119,18 +113,35 @@ pub(crate) fn inplace_argument( } } - // Without a static type system, only module-level functions could potentially be - // non-pandas calls. If they're not, `inplace` should be considered safe. - if is_checkable && !is_pandas { - return None; - } - - return Some(diagnostic); + checker.diagnostics.push(diagnostic); } // Duplicate keywords is a syntax error, so we can stop here. break; } } - None +} + +/// Remove the `inplace` argument from a function call and replace it with an +/// assignment. +fn convert_inplace_argument_to_assignment( + locator: &Locator, + expr: &Expr, + expr_range: TextRange, + args: &[Expr], + keywords: &[Keyword], +) -> Option { + // Add the assignment. + let call = expr.as_call_expr()?; + let attr = call.func.as_attribute_expr()?; + let insert_assignment = Edit::insertion( + format!("{name} = ", name = locator.slice(attr.value.range())), + expr.start(), + ); + + // Remove the `inplace` argument. + let remove_argument = + remove_argument(locator, call.func.end(), expr_range, args, keywords, false).ok()?; + + Some(Fix::suggested_edits(insert_assignment, [remove_argument])) } diff --git a/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs b/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs index b6c4120d7d..873d5c7f68 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs @@ -1,5 +1,6 @@ use rustpython_parser::ast::{self, Expr, Ranged}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -17,13 +18,14 @@ impl Violation for PandasUseOfPdMerge { } /// PD015 -pub(crate) fn use_of_pd_merge(func: &Expr) -> Option { +pub(crate) fn use_of_pd_merge(checker: &mut Checker, func: &Expr) { if let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func { if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() { if id == "pd" && attr == "merge" { - return Some(Diagnostic::new(PandasUseOfPdMerge, func.range())); + checker + .diagnostics + .push(Diagnostic::new(PandasUseOfPdMerge, func.range())); } } } - None } diff --git a/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap b/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap index b837655f46..513c426e64 100644 --- a/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap +++ b/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap @@ -8,7 +8,7 @@ PD002.py:5:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent b 5 | x.drop(["a"], axis=1, inplace=True) | ^^^^^^^^^^^^ PD002 6 | -7 | x.drop(["a"], axis=1, inplace=True) +7 | x.y.drop(["a"], axis=1, inplace=True) | = help: Assign to variable; remove `inplace` arg @@ -19,17 +19,17 @@ PD002.py:5:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent b 5 |-x.drop(["a"], axis=1, inplace=True) 5 |+x = x.drop(["a"], axis=1) 6 6 | -7 7 | x.drop(["a"], axis=1, inplace=True) +7 7 | x.y.drop(["a"], axis=1, inplace=True) 8 8 | -PD002.py:7:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:7:25: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | 5 | x.drop(["a"], axis=1, inplace=True) 6 | -7 | x.drop(["a"], axis=1, inplace=True) - | ^^^^^^^^^^^^ PD002 +7 | x.y.drop(["a"], axis=1, inplace=True) + | ^^^^^^^^^^^^ PD002 8 | -9 | x.drop( +9 | x["y"].drop(["a"], axis=1, inplace=True) | = help: Assign to variable; remove `inplace` arg @@ -37,104 +37,124 @@ PD002.py:7:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent b 4 4 | 5 5 | x.drop(["a"], axis=1, inplace=True) 6 6 | -7 |-x.drop(["a"], axis=1, inplace=True) - 7 |+x = x.drop(["a"], axis=1) +7 |-x.y.drop(["a"], axis=1, inplace=True) + 7 |+x.y = x.y.drop(["a"], axis=1) 8 8 | -9 9 | x.drop( -10 10 | inplace=True, +9 9 | x["y"].drop(["a"], axis=1, inplace=True) +10 10 | -PD002.py:10:5: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:9:28: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | - 9 | x.drop( -10 | inplace=True, - | ^^^^^^^^^^^^ PD002 -11 | columns=["a"], -12 | axis=1, + 7 | x.y.drop(["a"], axis=1, inplace=True) + 8 | + 9 | x["y"].drop(["a"], axis=1, inplace=True) + | ^^^^^^^^^^^^ PD002 +10 | +11 | x.drop( | = help: Assign to variable; remove `inplace` arg ℹ Suggested fix 6 6 | -7 7 | x.drop(["a"], axis=1, inplace=True) +7 7 | x.y.drop(["a"], axis=1, inplace=True) 8 8 | -9 |-x.drop( -10 |- inplace=True, - 9 |+x = x.drop( -11 10 | columns=["a"], -12 11 | axis=1, -13 12 | ) +9 |-x["y"].drop(["a"], axis=1, inplace=True) + 9 |+x["y"] = x["y"].drop(["a"], axis=1) +10 10 | +11 11 | x.drop( +12 12 | inplace=True, -PD002.py:17:9: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:12:5: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | -15 | if True: -16 | x.drop( -17 | inplace=True, +11 | x.drop( +12 | inplace=True, + | ^^^^^^^^^^^^ PD002 +13 | columns=["a"], +14 | axis=1, + | + = help: Assign to variable; remove `inplace` arg + +ℹ Suggested fix +8 8 | +9 9 | x["y"].drop(["a"], axis=1, inplace=True) +10 10 | +11 |-x.drop( +12 |- inplace=True, + 11 |+x = x.drop( +13 12 | columns=["a"], +14 13 | axis=1, +15 14 | ) + +PD002.py:19:9: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior + | +17 | if True: +18 | x.drop( +19 | inplace=True, | ^^^^^^^^^^^^ PD002 -18 | columns=["a"], -19 | axis=1, +20 | columns=["a"], +21 | axis=1, | = help: Assign to variable; remove `inplace` arg ℹ Suggested fix -13 13 | ) -14 14 | -15 15 | if True: -16 |- x.drop( -17 |- inplace=True, - 16 |+ x = x.drop( -18 17 | columns=["a"], -19 18 | axis=1, -20 19 | ) +15 15 | ) +16 16 | +17 17 | if True: +18 |- x.drop( +19 |- inplace=True, + 18 |+ x = x.drop( +20 19 | columns=["a"], +21 20 | axis=1, +22 21 | ) -PD002.py:22:33: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:24:33: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | -20 | ) -21 | -22 | x.drop(["a"], axis=1, **kwargs, inplace=True) +22 | ) +23 | +24 | x.drop(["a"], axis=1, **kwargs, inplace=True) | ^^^^^^^^^^^^ PD002 -23 | x.drop(["a"], axis=1, inplace=True, **kwargs) -24 | f(x.drop(["a"], axis=1, inplace=True)) +25 | x.drop(["a"], axis=1, inplace=True, **kwargs) +26 | f(x.drop(["a"], axis=1, inplace=True)) | = help: Assign to variable; remove `inplace` arg ℹ Suggested fix -19 19 | axis=1, -20 20 | ) -21 21 | -22 |-x.drop(["a"], axis=1, **kwargs, inplace=True) - 22 |+x = x.drop(["a"], axis=1, **kwargs) -23 23 | x.drop(["a"], axis=1, inplace=True, **kwargs) -24 24 | f(x.drop(["a"], axis=1, inplace=True)) -25 25 | +21 21 | axis=1, +22 22 | ) +23 23 | +24 |-x.drop(["a"], axis=1, **kwargs, inplace=True) + 24 |+x = x.drop(["a"], axis=1, **kwargs) +25 25 | x.drop(["a"], axis=1, inplace=True, **kwargs) +26 26 | f(x.drop(["a"], axis=1, inplace=True)) +27 27 | -PD002.py:23:23: PD002 `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:25:23: PD002 `inplace=True` should be avoided; it has inconsistent behavior | -22 | x.drop(["a"], axis=1, **kwargs, inplace=True) -23 | x.drop(["a"], axis=1, inplace=True, **kwargs) +24 | x.drop(["a"], axis=1, **kwargs, inplace=True) +25 | x.drop(["a"], axis=1, inplace=True, **kwargs) | ^^^^^^^^^^^^ PD002 -24 | f(x.drop(["a"], axis=1, inplace=True)) +26 | f(x.drop(["a"], axis=1, inplace=True)) | = help: Assign to variable; remove `inplace` arg -PD002.py:24:25: PD002 `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:26:25: PD002 `inplace=True` should be avoided; it has inconsistent behavior | -22 | x.drop(["a"], axis=1, **kwargs, inplace=True) -23 | x.drop(["a"], axis=1, inplace=True, **kwargs) -24 | f(x.drop(["a"], axis=1, inplace=True)) +24 | x.drop(["a"], axis=1, **kwargs, inplace=True) +25 | x.drop(["a"], axis=1, inplace=True, **kwargs) +26 | f(x.drop(["a"], axis=1, inplace=True)) | ^^^^^^^^^^^^ PD002 -25 | -26 | x.apply(lambda x: x.sort_values('a', inplace=True)) +27 | +28 | x.apply(lambda x: x.sort_values("a", inplace=True)) | = help: Assign to variable; remove `inplace` arg -PD002.py:26:38: PD002 `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:28:38: PD002 `inplace=True` should be avoided; it has inconsistent behavior | -24 | f(x.drop(["a"], axis=1, inplace=True)) -25 | -26 | x.apply(lambda x: x.sort_values('a', inplace=True)) +26 | f(x.drop(["a"], axis=1, inplace=True)) +27 | +28 | x.apply(lambda x: x.sort_values("a", inplace=True)) | ^^^^^^^^^^^^ PD002 -27 | import torch -28 | torch.m.ReLU(inplace=True) # safe because this isn't a pandas call +29 | import torch | = help: Assign to variable; remove `inplace` arg diff --git a/crates/ruff/src/rules/pep8_naming/helpers.rs b/crates/ruff/src/rules/pep8_naming/helpers.rs index ac6d369b38..24017389ff 100644 --- a/crates/ruff/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff/src/rules/pep8_naming/helpers.rs @@ -26,7 +26,7 @@ pub(super) fn is_named_tuple_assignment(stmt: &Stmt, semantic: &SemanticModel) - let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; - let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { @@ -41,7 +41,7 @@ pub(super) fn is_typed_dict_assignment(stmt: &Stmt, semantic: &SemanticModel) -> let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; - let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { @@ -53,7 +53,7 @@ pub(super) fn is_type_var_assignment(stmt: &Stmt, semantic: &SemanticModel) -> b let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; - let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { diff --git a/crates/ruff/src/rules/perflint/mod.rs b/crates/ruff/src/rules/perflint/mod.rs index 33c9691206..291bfcd207 100644 --- a/crates/ruff/src/rules/perflint/mod.rs +++ b/crates/ruff/src/rules/perflint/mod.rs @@ -16,6 +16,8 @@ mod tests { #[test_case(Rule::UnnecessaryListCast, Path::new("PERF101.py"))] #[test_case(Rule::IncorrectDictIterator, Path::new("PERF102.py"))] #[test_case(Rule::TryExceptInLoop, Path::new("PERF203.py"))] + #[test_case(Rule::ManualListComprehension, Path::new("PERF401.py"))] + #[test_case(Rule::ManualListCopy, Path::new("PERF402.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs index c2af990708..af2677f2f1 100644 --- a/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs +++ b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs @@ -23,6 +23,10 @@ use crate::registry::AsRule; /// avoid allocating tuples for every item in the dictionary. They also /// communicate the intent of the code more clearly. /// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// /// ## Example /// ```python /// some_dict = {"a": 1, "b": 2} @@ -56,12 +60,8 @@ impl AlwaysAutofixableViolation for IncorrectDictIterator { /// PERF102 pub(crate) fn incorrect_dict_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) { - let Expr::Tuple(ast::ExprTuple { - elts, - .. - }) = target - else { - return + let Expr::Tuple(ast::ExprTuple { elts, .. }) = target else { + return; }; if elts.len() != 2 { return; @@ -72,7 +72,7 @@ pub(crate) fn incorrect_dict_iterator(checker: &mut Checker, target: &Expr, iter if !args.is_empty() { return; } - let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else { + let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else { return; }; if attr != "items" { diff --git a/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs b/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs new file mode 100644 index 0000000000..eb6e56735b --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs @@ -0,0 +1,120 @@ +use rustpython_parser::ast::{self, Expr, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `for` loops that can be replaced by a list comprehension. +/// +/// ## Why is this bad? +/// When creating a transformed list from an existing list using a for-loop, +/// prefer a list comprehension. List comprehensions are more readable and +/// more performant. +/// +/// Using the below as an example, the list comprehension is ~10% faster on +/// Python 3.11, and ~25% faster on Python 3.10. +/// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// +/// ## Example +/// ```python +/// original = list(range(10000)) +/// filtered = [] +/// for i in original: +/// if i % 2: +/// filtered.append(i) +/// ``` +/// +/// Use instead: +/// ```python +/// original = list(range(10000)) +/// filtered = [x for x in original if x % 2] +/// ``` +/// +/// If you're appending to an existing list, use the `extend` method instead: +/// ```python +/// original = list(range(10000)) +/// filtered.extend(x for x in original if x % 2) +/// ``` +#[violation] +pub struct ManualListComprehension; + +impl Violation for ManualListComprehension { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use a list comprehension to create a transformed list") + } +} + +/// PERF401 +pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, body: &[Stmt]) { + let (stmt, conditional) = match body { + // ```python + // for x in y: + // if z: + // filtered.append(x) + // ``` + [Stmt::If(ast::StmtIf { body, orelse, .. })] => { + if !orelse.is_empty() { + return; + } + let [stmt] = body.as_slice() else { + return; + }; + (stmt, true) + } + // ```python + // for x in y: + // filtered.append(f(x)) + // ``` + [stmt] => (stmt, false), + _ => return, + }; + + let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { + return; + }; + + let Expr::Call(ast::ExprCall { + func, + range, + args, + keywords, + }) = value.as_ref() + else { + return; + }; + + if !keywords.is_empty() { + return; + } + + let [arg] = args.as_slice() else { + return; + }; + + // Ignore direct list copies (e.g., `for x in y: filtered.append(x)`). + if !conditional { + if arg.as_name_expr().map_or(false, |arg| { + target + .as_name_expr() + .map_or(false, |target| arg.id == target.id) + }) { + return; + } + } + + let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { + return; + }; + + if attr.as_str() == "append" { + checker + .diagnostics + .push(Diagnostic::new(ManualListComprehension, *range)); + } +} diff --git a/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs b/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs new file mode 100644 index 0000000000..13554488bd --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs @@ -0,0 +1,92 @@ +use rustpython_parser::ast::{self, Expr, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `for` loops that can be replaced by a making a copy of a list. +/// +/// ## Why is this bad? +/// When creating a copy of an existing list using a for-loop, prefer +/// `list` or `list.copy` instead. Making a direct copy is more readable and +/// more performant. +/// +/// Using the below as an example, the `list`-based copy is ~2x faster on +/// Python 3.11. +/// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// +/// ## Example +/// ```python +/// original = list(range(10000)) +/// filtered = [] +/// for i in original: +/// filtered.append(i) +/// ``` +/// +/// Use instead: +/// ```python +/// original = list(range(10000)) +/// filtered = list(original) +/// ``` +#[violation] +pub struct ManualListCopy; + +impl Violation for ManualListCopy { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use `list` or `list.copy` to create a copy of a list") + } +} + +/// PERF402 +pub(crate) fn manual_list_copy(checker: &mut Checker, target: &Expr, body: &[Stmt]) { + let [stmt] = body else { + return; + }; + + let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { + return; + }; + + let Expr::Call(ast::ExprCall { + func, + range, + args, + keywords, + }) = value.as_ref() + else { + return; + }; + + if !keywords.is_empty() { + return; + } + + let [arg] = args.as_slice() else { + return; + }; + + // Only flag direct list copies (e.g., `for x in y: filtered.append(x)`). + if !arg.as_name_expr().map_or(false, |arg| { + target + .as_name_expr() + .map_or(false, |target| arg.id == target.id) + }) { + return; + } + + let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { + return; + }; + + if matches!(attr.as_str(), "append" | "insert") { + checker + .diagnostics + .push(Diagnostic::new(ManualListCopy, *range)); + } +} diff --git a/crates/ruff/src/rules/perflint/rules/mod.rs b/crates/ruff/src/rules/perflint/rules/mod.rs index 4af80c1432..690b0fc1fe 100644 --- a/crates/ruff/src/rules/perflint/rules/mod.rs +++ b/crates/ruff/src/rules/perflint/rules/mod.rs @@ -1,7 +1,11 @@ pub(crate) use incorrect_dict_iterator::*; +pub(crate) use manual_list_comprehension::*; +pub(crate) use manual_list_copy::*; pub(crate) use try_except_in_loop::*; pub(crate) use unnecessary_list_cast::*; mod incorrect_dict_iterator; +mod manual_list_comprehension; +mod manual_list_copy; mod try_except_in_loop; mod unnecessary_list_cast; diff --git a/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs b/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs index 9eaaea1dc5..670256d10a 100644 --- a/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs +++ b/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs @@ -19,6 +19,10 @@ use crate::registry::AsRule; /// Removing the `list()` call will not change the behavior of the code, but /// may improve performance. /// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// /// ## Example /// ```python /// items = (1, 2, 3) @@ -48,7 +52,13 @@ impl AlwaysAutofixableViolation for UnnecessaryListCast { /// PERF101 pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr) { - let Expr::Call(ast::ExprCall{ func, args, range: list_range, ..}) = iter else { + let Expr::Call(ast::ExprCall { + func, + args, + range: list_range, + .. + }) = iter + else { return; }; @@ -56,7 +66,7 @@ pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr) { return; } - let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else{ + let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else { return; }; diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap new file mode 100644 index 0000000000..cf2e2677c5 --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF401.py:6:13: PERF401 Use a list comprehension to create a transformed list + | +4 | for i in items: +5 | if i % 2: +6 | result.append(i) # PERF401 + | ^^^^^^^^^^^^^^^^ PERF401 + | + +PERF401.py:13:9: PERF401 Use a list comprehension to create a transformed list + | +11 | result = [] +12 | for i in items: +13 | result.append(i * i) # PERF401 + | ^^^^^^^^^^^^^^^^^^^^ PERF401 + | + + diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap new file mode 100644 index 0000000000..e56584c95e --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF402.py:5:9: PERF402 Use `list` or `list.copy` to create a copy of a list + | +3 | result = [] +4 | for i in items: +5 | result.append(i) # PERF402 + | ^^^^^^^^^^^^^^^^ PERF402 + | + + diff --git a/crates/ruff/src/rules/pydocstyle/mod.rs b/crates/ruff/src/rules/pydocstyle/mod.rs index ca0035e418..41c6a1fc6f 100644 --- a/crates/ruff/src/rules/pydocstyle/mod.rs +++ b/crates/ruff/src/rules/pydocstyle/mod.rs @@ -51,6 +51,7 @@ mod tests { #[test_case(Rule::EmptyDocstring, Path::new("D.py"))] #[test_case(Rule::EmptyDocstringSection, Path::new("sections.py"))] #[test_case(Rule::NonImperativeMood, Path::new("D401.py"))] + #[test_case(Rule::NoBlankLineAfterSection, Path::new("D410.py"))] #[test_case(Rule::OneBlankLineAfterClass, Path::new("D.py"))] #[test_case(Rule::OneBlankLineBeforeClass, Path::new("D.py"))] #[test_case(Rule::UndocumentedPublicClass, Path::new("D.py"))] diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs index 3d4524d497..d13f094e24 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs @@ -161,10 +161,11 @@ impl AlwaysAutofixableViolation for BlankLineBeforeClass { /// D203, D204, D211 pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstring) { let Definition::Member(Member { - kind: MemberKind::Class | MemberKind::NestedClass , + kind: MemberKind::Class | MemberKind::NestedClass, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs index 44bea30595..ccdeed72b7 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs @@ -107,7 +107,8 @@ pub(crate) fn blank_before_after_function(checker: &mut Checker, docstring: &Doc kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs index 1e5c110c47..b17ef0b3cf 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs @@ -69,7 +69,7 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) { let body = docstring.body(); let Some(first_word) = body.split(' ').next() else { - return + return; }; // Like pydocstyle, we only support ASCII for now. diff --git a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs index 87262ccce1..ee9e2364ea 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs @@ -84,7 +84,8 @@ pub(crate) fn if_needed(checker: &mut Checker, docstring: &Docstring) { kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; if !is_overload(cast::decorator_list(stmt), checker.semantic()) { diff --git a/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs b/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs index b470da5ea7..7660553e96 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs @@ -132,10 +132,7 @@ pub(crate) fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstr }; let mut content_lines = UniversalNewlineIterator::with_offset(contents, docstring.start()); - let Some(first_line) = content_lines - .next() - else - { + let Some(first_line) = content_lines.next() else { return; }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs index 486df63860..11054ab016 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs @@ -58,7 +58,8 @@ pub(crate) fn no_signature(checker: &mut Checker, docstring: &Docstring) { kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; let Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) = stmt else { diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index eb2676b1c9..4dd426f4b5 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -1715,18 +1715,18 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; - let ( - Stmt::FunctionDef(ast::StmtFunctionDef { - args: arguments, .. - }) - | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - args: arguments, .. - }) - ) = stmt else { + let (Stmt::FunctionDef(ast::StmtFunctionDef { + args: arguments, .. + }) + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { + args: arguments, .. + })) = stmt + else { return; }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs b/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs index eccabe28ec..4e73aa0ed3 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs @@ -58,7 +58,7 @@ pub(crate) fn starts_with_this(checker: &mut Checker, docstring: &Docstring) { } let Some(first_word) = trimmed.split(' ').next() else { - return + return; }; if normalize_word(first_word) != "this" { return; diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_D410.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_D410.py.snap new file mode 100644 index 0000000000..0ce1725147 --- /dev/null +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_D410.py.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff/src/rules/pydocstyle/mod.rs +--- +D410.py:2:5: D410 [*] Missing blank line after section ("Parameters") + | + 1 | def f(a: int, b: int) -> int: + 2 | """Showcase function. + | _____^ + 3 | | + 4 | | Parameters + 5 | | ---------- + 6 | | a : int + 7 | | _description_ + 8 | | b : int + 9 | | _description_ +10 | | Returns +11 | | ------- +12 | | int +13 | | _description +14 | | """ + | |_______^ D410 +15 | return b - a + | + = help: Add blank line after "Parameters" + +ℹ Fix +7 7 | _description_ +8 8 | b : int +9 9 | _description_ + 10 |+ +10 11 | Returns +11 12 | ------- +12 13 | int + +D410.py:19:5: D410 [*] Missing blank line after section ("Parameters") + | +18 | def f() -> int: +19 | """Showcase function. + | _____^ +20 | | +21 | | Parameters +22 | | ---------- +23 | | Returns +24 | | ------- +25 | | """ + | |_______^ D410 + | + = help: Add blank line after "Parameters" + +ℹ Fix +20 20 | +21 21 | Parameters +22 22 | ---------- + 23 |+ +23 24 | Returns +24 25 | ------- +25 26 | """ + + diff --git a/crates/ruff/src/rules/pyflakes/cformat.rs b/crates/ruff/src/rules/pyflakes/cformat.rs index 1a7c3f6d9e..1f9279c241 100644 --- a/crates/ruff/src/rules/pyflakes/cformat.rs +++ b/crates/ruff/src/rules/pyflakes/cformat.rs @@ -25,8 +25,8 @@ impl From<&CFormatString> for CFormatSummary { ref min_field_width, ref precision, .. - }) = format_part.1 else - { + }) = format_part.1 + else { continue; }; match mapping_key { diff --git a/crates/ruff/src/rules/pyflakes/format.rs b/crates/ruff/src/rules/pyflakes/format.rs index 03d1f30f95..abb52e8c8a 100644 --- a/crates/ruff/src/rules/pyflakes/format.rs +++ b/crates/ruff/src/rules/pyflakes/format.rs @@ -44,7 +44,8 @@ impl TryFrom<&str> for FormatSummary { field_name, format_spec, .. - } = format_part else { + } = format_part + else { continue; }; let parsed = FieldName::parse(field_name)?; diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 0bdbeaee1d..e796ac4563 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -490,7 +490,7 @@ mod tests { "load_after_unbind_from_class_scope" )] fn contents(contents: &str, snapshot: &str) { - let diagnostics = test_snippet(contents, &Settings::for_rules(&Linter::Pyflakes)); + let diagnostics = test_snippet(contents, &Settings::for_rules(Linter::Pyflakes.rules())); assert_messages!(snapshot, diagnostics); } @@ -498,7 +498,7 @@ mod tests { /// Note that all tests marked with `#[ignore]` should be considered TODOs. fn flakes(contents: &str, expected: &[Rule]) { let contents = dedent(contents); - let settings = Settings::for_rules(&Linter::Pyflakes); + let settings = Settings::for_rules(Linter::Pyflakes.rules()); let tokens: Vec = ruff_rustpython::tokenize(&contents); let locator = Locator::new(&contents); let stylist = Stylist::from_tokens(&tokens, &locator); diff --git a/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs b/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs index 54fab4be14..f57f092ce5 100644 --- a/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs @@ -9,7 +9,7 @@ use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; #[derive(Debug, PartialEq, Eq)] -pub(crate) enum DeferralKeyword { +enum DeferralKeyword { Yield, YieldFrom, Await, diff --git a/crates/ruff/src/rules/pylint/helpers.rs b/crates/ruff/src/rules/pylint/helpers.rs index 8af22107a3..51d9360558 100644 --- a/crates/ruff/src/rules/pylint/helpers.rs +++ b/crates/ruff/src/rules/pylint/helpers.rs @@ -10,18 +10,17 @@ use crate::settings::Settings; pub(super) fn in_dunder_init(semantic: &SemanticModel, settings: &Settings) -> bool { let scope = semantic.scope(); - let ( - ScopeKind::Function(ast::StmtFunctionDef { - name, - decorator_list, + let (ScopeKind::Function(ast::StmtFunctionDef { + name, + decorator_list, .. - }) | - ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { - name, - decorator_list, - .. - }) - ) = scope.kind else { + }) + | ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { + name, + decorator_list, + .. + })) = scope.kind + else { return false; }; if name != "__init__" { diff --git a/crates/ruff/src/rules/pylint/rules/import_self.rs b/crates/ruff/src/rules/pylint/rules/import_self.rs index a06f84bfe4..63ba4cfb99 100644 --- a/crates/ruff/src/rules/pylint/rules/import_self.rs +++ b/crates/ruff/src/rules/pylint/rules/import_self.rs @@ -60,7 +60,8 @@ pub(crate) fn import_from_self( let Some(module_path) = module_path else { return None; }; - let Some(imported_module_path) = resolve_imported_module_path(level, module, Some(module_path)) else { + let Some(imported_module_path) = resolve_imported_module_path(level, module, Some(module_path)) + else { return None; }; diff --git a/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs b/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs index ccb4113967..b8c78baab8 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs @@ -94,7 +94,12 @@ pub(crate) fn invalid_envvar_default( let Some(expr) = args.get(1).or_else(|| { keywords .iter() - .find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg .as_str()== "default")) + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |arg| arg.as_str() == "default") + }) .map(|keyword| &keyword.value) }) else { return; diff --git a/crates/ruff/src/rules/pylint/rules/nested_min_max.rs b/crates/ruff/src/rules/pylint/rules/nested_min_max.rs index 1ca5ad354a..05cc249fc3 100644 --- a/crates/ruff/src/rules/pylint/rules/nested_min_max.rs +++ b/crates/ruff/src/rules/pylint/rules/nested_min_max.rs @@ -143,7 +143,7 @@ pub(crate) fn nested_min_max( } if args.iter().any(|arg| { - let Expr::Call(ast::ExprCall { func, keywords, ..} )= arg else { + let Expr::Call(ast::ExprCall { func, keywords, .. }) = arg else { return false; }; MinMax::try_from_call(func.as_ref(), keywords.as_ref(), checker.semantic()) == Some(min_max) diff --git a/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs b/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs index eb02b3cc9f..e77b612130 100644 --- a/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs +++ b/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs @@ -160,7 +160,9 @@ pub(crate) fn unexpected_special_method_signature( let actual_params = args.args.len(); let mandatory_params = args.args.iter().filter(|arg| arg.default.is_none()).count(); - let Some(expected_params) = ExpectedParams::from_method(name, is_staticmethod(decorator_list, checker.semantic())) else { + let Some(expected_params) = + ExpectedParams::from_method(name, is_staticmethod(decorator_list, checker.semantic())) + else { return; }; diff --git a/crates/ruff/src/rules/pylint/rules/useless_return.rs b/crates/ruff/src/rules/pylint/rules/useless_return.rs index 58b6b1b46e..28e7c689f4 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_return.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_return.rs @@ -85,7 +85,7 @@ pub(crate) fn useless_return<'a>( } // Verify that the last statement is a return statement. - let Stmt::Return(ast::StmtReturn { value, range: _}) = &last_stmt else { + let Stmt::Return(ast::StmtReturn { value, range: _ }) = &last_stmt else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/mod.rs b/crates/ruff/src/rules/pyupgrade/mod.rs index 260ee6af1c..f65042ebc9 100644 --- a/crates/ruff/src/rules/pyupgrade/mod.rs +++ b/crates/ruff/src/rules/pyupgrade/mod.rs @@ -2,6 +2,7 @@ mod fixes; mod helpers; pub(crate) mod rules; +pub mod settings; pub(crate) mod types; #[cfg(test)] @@ -12,6 +13,7 @@ mod tests { use test_case::test_case; use crate::registry::Rule; + use crate::rules::pyupgrade; use crate::settings::types::PythonVersion; use crate::test::test_path; use crate::{assert_messages, settings}; @@ -85,6 +87,38 @@ mod tests { Ok(()) } + #[test] + fn future_annotations_keep_runtime_typing_p37() -> Result<()> { + let diagnostics = test_path( + Path::new("pyupgrade/future_annotations.py"), + &settings::Settings { + pyupgrade: pyupgrade::settings::Settings { + keep_runtime_typing: true, + }, + target_version: PythonVersion::Py37, + ..settings::Settings::for_rule(Rule::NonPEP585Annotation) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + + #[test] + fn future_annotations_keep_runtime_typing_p310() -> Result<()> { + let diagnostics = test_path( + Path::new("pyupgrade/future_annotations.py"), + &settings::Settings { + pyupgrade: pyupgrade::settings::Settings { + keep_runtime_typing: true, + }, + target_version: PythonVersion::Py310, + ..settings::Settings::for_rule(Rule::NonPEP585Annotation) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + #[test] fn future_annotations_pep_585_p37() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs index 284342608f..63298260cf 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs @@ -81,7 +81,8 @@ fn match_named_tuple_assign<'a>( args, keywords, range: _, - }) = value else { + }) = value + else { return None; }; if !semantic.match_typing_expr(func, "NamedTuple") { @@ -136,10 +137,12 @@ fn match_defaults(keywords: &[Keyword]) -> Result<&[Expr]> { /// Create a list of property assignments from the `NamedTuple` arguments. fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result> { let Some(fields) = args.get(1) else { - let node = Stmt::Pass(ast::StmtPass { range: TextRange::default()}); + let node = Stmt::Pass(ast::StmtPass { + range: TextRange::default(), + }); return Ok(vec![node]); }; - let Expr::List(ast::ExprList { elts, .. } )= &fields else { + let Expr::List(ast::ExprList { elts, .. }) = &fields else { bail!("Expected argument to be `Expr::List`"); }; if elts.is_empty() { @@ -167,7 +170,8 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result( func, args, keywords, - range: _ - }) = value else { + range: _, + }) = value + else { return None; }; if !semantic.match_typing_expr(func, "TypedDict") { @@ -205,7 +206,7 @@ fn properties_from_keywords(keywords: &[Keyword]) -> Result> { fn match_total_from_only_keyword(keywords: &[Keyword]) -> Option<&Keyword> { keywords.iter().find(|keyword| { let Some(arg) = &keyword.arg else { - return false + return false; }; arg.as_str() == "total" }) @@ -271,8 +272,8 @@ pub(crate) fn convert_typed_dict_functional_to_class( value: &Expr, ) { let Some((class_name, args, keywords, base_class)) = - match_typed_dict_assign(targets, value, checker.semantic()) else - { + match_typed_dict_assign(targets, value, checker.semantic()) + else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index 314df7705c..4dac80b3e4 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -471,7 +471,7 @@ impl<'a> ImportReplacer<'a> { // line, we can't add a statement after it. For example, if we have // `if True: import foo`, we can't add a statement to the next line. let Some(indentation) = indentation else { - let operation = WithoutRename { + let operation = WithoutRename { target: target.to_string(), members: matched_names .iter() diff --git a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs index 6bdcee9b63..f5d67ae166 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs @@ -106,7 +106,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if i >= tokens.len() { return None; } - let Ok(( tok, _)) = &tokens[i] else { + let Ok((tok, _)) = &tokens[i] else { return None; }; match tok { @@ -122,7 +122,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if i >= tokens.len() { return None; } - let Ok(( tok, _)) = &tokens[i] else { + let Ok((tok, _)) = &tokens[i] else { return None; }; if matches!(tok, Tok::Rpar) { diff --git a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs index bacfc9c4c2..9420335056 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs @@ -197,7 +197,7 @@ fn try_convert_to_f_string(expr: &Expr, locator: &Locator) -> Option { return None; }; - let Some(mut summary) = FormatSummaryValues::try_from_expr( expr, locator) else { + let Some(mut summary) = FormatSummaryValues::try_from_expr(expr, locator) else { return None; }; @@ -325,7 +325,7 @@ pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &E // Currently, the only issue we know of is in LibCST: // https://github.com/Instagram/LibCST/issues/846 - let Some(mut contents) = try_convert_to_f_string( expr, checker.locator) else { + let Some(mut contents) = try_convert_to_f_string(expr, checker.locator) else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs index 52a38c173d..29da844358 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs @@ -64,7 +64,8 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: args, keywords, range: _, - }) = &decorator.expression else { + }) = &decorator.expression + else { continue; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs index 2b0058fdcf..1a42ab66dc 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs @@ -62,7 +62,8 @@ pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list args, keywords, range: _, - }) = &decorator.expression else { + }) = &decorator.expression + else { continue; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs index 365cba7d26..e9265ee090 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs @@ -88,11 +88,16 @@ pub(crate) fn native_literals( if (id == "str" || id == "bytes") && checker.semantic().is_builtin(id) { let Some(arg) = args.get(0) else { - let mut diagnostic = Diagnostic::new(NativeLiterals{literal_type:if id == "str" { - LiteralType::Str - } else { - LiteralType::Bytes - }}, expr.range()); + let mut diagnostic = Diagnostic::new( + NativeLiterals { + literal_type: if id == "str" { + LiteralType::Str + } else { + LiteralType::Bytes + }, + }, + expr.range(), + ); if checker.patch(diagnostic.kind.rule()) { let constant = if id == "bytes" { Constant::Bytes(vec![]) diff --git a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs index 8fad415be1..e84e0ab64b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -223,7 +223,11 @@ fn fix_py2_block( let parent = checker.semantic().stmt_parent(); let edit = delete_stmt( stmt, - if matches!(block.leading_token.tok, StartTok::If) { parent } else { None }, + if matches!(block.leading_token.tok, StartTok::If) { + parent + } else { + None + }, checker.locator, checker.indexer, ); @@ -348,7 +352,8 @@ pub(crate) fn outdated_version_block( ops, comparators, range: _, - }) = &test else { + }) = &test + else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs index 01592e3c29..a94eed542e 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -69,7 +69,7 @@ fn generate_fix( Edit::range_replacement("capture_output=True".to_string(), first.range()), [remove_argument( locator, - func.start(), + func.end(), second.range(), args, keywords, diff --git a/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs b/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs index fab3d45868..2362552fad 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs @@ -99,22 +99,27 @@ pub(crate) fn super_call_with_parameters( // Find the enclosing function definition (if any). let Some(Stmt::FunctionDef(ast::StmtFunctionDef { args: parent_args, .. - })) = parents.find(|stmt| stmt.is_function_def_stmt()) else { + })) = parents.find(|stmt| stmt.is_function_def_stmt()) + else { return; }; // Extract the name of the first argument to the enclosing function. let Some(ArgWithDefault { - def: Arg { arg: parent_arg, .. }, + def: Arg { + arg: parent_arg, .. + }, .. - }) = parent_args.args.first() else { + }) = parent_args.args.first() + else { return; }; // Find the enclosing class definition (if any). let Some(Stmt::ClassDef(ast::StmtClassDef { name: parent_name, .. - })) = parents.find(|stmt| stmt.is_class_def_stmt()) else { + })) = parents.find(|stmt| stmt.is_class_def_stmt()) + else { return; }; @@ -125,7 +130,8 @@ pub(crate) fn super_call_with_parameters( Expr::Name(ast::ExprName { id: second_arg_id, .. }), - ) = (first_arg, second_arg) else { + ) = (first_arg, second_arg) + else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs index 81e3f40daf..9be33ea2c0 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -66,7 +66,7 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, { return; } - let Expr::Constant(ast::ExprConstant { value, .. } )= &args[0] else { + let Expr::Constant(ast::ExprConstant { value, .. }) = &args[0] else { return; }; let Some(primitive) = Primitive::from_constant(value) else { diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs index 5fcb60fa8c..69a27b27b4 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs @@ -1,11 +1,10 @@ use std::ops::Add; use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::{self, Stmt}; +use rustpython_parser::ast::{self, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -44,16 +43,12 @@ impl AlwaysAutofixableViolation for UnnecessaryClassParentheses { } /// UP039 -pub(crate) fn unnecessary_class_parentheses( - checker: &mut Checker, - class_def: &ast::StmtClassDef, - stmt: &Stmt, -) { +pub(crate) fn unnecessary_class_parentheses(checker: &mut Checker, class_def: &ast::StmtClassDef) { if !class_def.bases.is_empty() || !class_def.keywords.is_empty() { return; } - let offset = stmt.identifier().start(); + let offset = class_def.name.end(); let contents = checker.locator.after(offset); // Find the open and closing parentheses between the class name and the colon, if they exist. diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs index ed48bfc99b..c210724341 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs @@ -61,7 +61,8 @@ fn match_encoded_variable(func: &Expr) -> Option<&Expr> { value: variable, attr, .. - }) = func else { + }) = func + else { return None; }; if attr != "encode" { @@ -190,7 +191,7 @@ pub(crate) fn unnecessary_encode_utf8( diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), kwarg.range(), args, kwargs, @@ -212,7 +213,7 @@ pub(crate) fn unnecessary_encode_utf8( diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), arg.range(), args, kwargs, @@ -241,7 +242,7 @@ pub(crate) fn unnecessary_encode_utf8( diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), kwarg.range(), args, kwargs, @@ -263,7 +264,7 @@ pub(crate) fn unnecessary_encode_utf8( diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), arg.range(), args, kwargs, diff --git a/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs b/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs index 939b5e98bd..193fe237a2 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs @@ -56,7 +56,8 @@ pub(crate) fn unpacked_list_comprehension(checker: &mut Checker, targets: &[Expr elt, generators, range: _, - }) = value else { + }) = value + else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs index bb54016701..286c4116f1 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs @@ -52,13 +52,13 @@ pub(crate) fn useless_metaclass_type( return; } let Expr::Name(ast::ExprName { id, .. }) = targets.first().unwrap() else { - return ; + return; }; if id != "__metaclass__" { return; } let Expr::Name(ast::ExprName { id, .. }) = value else { - return ; + return; }; if id != "type" { return; diff --git a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs index aa6ec0e31b..b3ac4c2899 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -1,8 +1,7 @@ -use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::identifier::Identifier; use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; @@ -47,11 +46,7 @@ impl AlwaysAutofixableViolation for UselessObjectInheritance { } /// UP004 -pub(crate) fn useless_object_inheritance( - checker: &mut Checker, - class_def: &ast::StmtClassDef, - stmt: &Stmt, -) { +pub(crate) fn useless_object_inheritance(checker: &mut Checker, class_def: &ast::StmtClassDef) { for expr in &class_def.bases { let Expr::Name(ast::ExprName { id, .. }) = expr else { continue; @@ -73,7 +68,7 @@ pub(crate) fn useless_object_inheritance( diagnostic.try_set_fix(|| { let edit = remove_argument( checker.locator, - stmt.identifier().start(), + class_def.name.end(), expr.range(), &class_def.bases, &class_def.keywords, diff --git a/crates/ruff/src/rules/pyupgrade/settings.rs b/crates/ruff/src/rules/pyupgrade/settings.rs new file mode 100644 index 0000000000..a5b2d78188 --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/settings.rs @@ -0,0 +1,76 @@ +//! Settings for the `pyupgrade` plugin. + +use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, CombineOptions, +)] +#[serde( + deny_unknown_fields, + rename_all = "kebab-case", + rename = "PyUpgradeOptions" +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Options { + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + # Preserve types, even if a file imports `from __future__ import annotations`. + keep-runtime-typing = true + "# + )] + /// Whether to avoid PEP 585 (`List[int]` -> `list[int]`) and PEP 604 + /// (`Union[str, int]` -> `str | int`) rewrites even if a file imports + /// `from __future__ import annotations`. + /// + /// This setting is only applicable when the target Python version is below + /// 3.9 and 3.10 respectively, and is most commonly used when working with + /// libraries like Pydantic and FastAPI, which rely on the ability to parse + /// type annotations at runtime. The use of `from __future__ import annotations` + /// causes Python to treat the type annotations as strings, which typically + /// allows for the use of language features that appear in later Python + /// versions but are not yet supported by the current version (e.g., `str | + /// int`). However, libraries that rely on runtime type annotations will + /// break if the annotations are incompatible with the current Python + /// version. + /// + /// For example, while the following is valid Python 3.8 code due to the + /// presence of `from __future__ import annotations`, the use of `str| int` + /// prior to Python 3.10 will cause Pydantic to raise a `TypeError` at + /// runtime: + /// + /// ```python + /// from __future__ import annotations + /// + /// import pydantic + /// + /// class Foo(pydantic.BaseModel): + /// bar: str | int + /// ``` + /// + /// + pub keep_runtime_typing: Option, +} + +#[derive(Debug, Default, CacheKey)] +pub struct Settings { + pub keep_runtime_typing: bool, +} + +impl From for Settings { + fn from(options: Options) -> Self { + Self { + keep_runtime_typing: options.keep_runtime_typing.unwrap_or_default(), + } + } +} + +impl From for Options { + fn from(settings: Settings) -> Self { + Self { + keep_runtime_typing: Some(settings.keep_runtime_typing), + } + } +} diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap index 98d0ed5f7a..d25beae3d4 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap @@ -452,6 +452,8 @@ UP012.py:69:1: UP012 [*] Unnecessary call to `encode` as UTF-8 71 | | "def" 72 | | )).encode() | |___________^ UP012 +73 | +74 | (f"foo{bar}").encode("utf-8") | = help: Rewrite as bytes literal @@ -465,5 +467,82 @@ UP012.py:69:1: UP012 [*] Unnecessary call to `encode` as UTF-8 70 |+ b"abc" 71 |+ b"def" 72 |+)) +73 73 | +74 74 | (f"foo{bar}").encode("utf-8") +75 75 | (f"foo{bar}").encode(encoding="utf-8") + +UP012.py:74:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +72 | )).encode() +73 | +74 | (f"foo{bar}").encode("utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 +75 | (f"foo{bar}").encode(encoding="utf-8") +76 | ("unicode text©").encode("utf-8") + | + = help: Remove unnecessary encoding argument + +ℹ Fix +71 71 | "def" +72 72 | )).encode() +73 73 | +74 |-(f"foo{bar}").encode("utf-8") + 74 |+(f"foo{bar}").encode() +75 75 | (f"foo{bar}").encode(encoding="utf-8") +76 76 | ("unicode text©").encode("utf-8") +77 77 | ("unicode text©").encode(encoding="utf-8") + +UP012.py:75:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +74 | (f"foo{bar}").encode("utf-8") +75 | (f"foo{bar}").encode(encoding="utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 +76 | ("unicode text©").encode("utf-8") +77 | ("unicode text©").encode(encoding="utf-8") + | + = help: Remove unnecessary encoding argument + +ℹ Fix +72 72 | )).encode() +73 73 | +74 74 | (f"foo{bar}").encode("utf-8") +75 |-(f"foo{bar}").encode(encoding="utf-8") + 75 |+(f"foo{bar}").encode() +76 76 | ("unicode text©").encode("utf-8") +77 77 | ("unicode text©").encode(encoding="utf-8") + +UP012.py:76:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +74 | (f"foo{bar}").encode("utf-8") +75 | (f"foo{bar}").encode(encoding="utf-8") +76 | ("unicode text©").encode("utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 +77 | ("unicode text©").encode(encoding="utf-8") + | + = help: Remove unnecessary encoding argument + +ℹ Fix +73 73 | +74 74 | (f"foo{bar}").encode("utf-8") +75 75 | (f"foo{bar}").encode(encoding="utf-8") +76 |-("unicode text©").encode("utf-8") + 76 |+("unicode text©").encode() +77 77 | ("unicode text©").encode(encoding="utf-8") + +UP012.py:77:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +75 | (f"foo{bar}").encode(encoding="utf-8") +76 | ("unicode text©").encode("utf-8") +77 | ("unicode text©").encode(encoding="utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 + | + = help: Remove unnecessary encoding argument + +ℹ Fix +74 74 | (f"foo{bar}").encode("utf-8") +75 75 | (f"foo{bar}").encode(encoding="utf-8") +76 76 | ("unicode text©").encode("utf-8") +77 |-("unicode text©").encode(encoding="utf-8") + 77 |+("unicode text©").encode() diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap new file mode 100644 index 0000000000..c9972fb6d9 --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap @@ -0,0 +1,75 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +--- +future_annotations.py:34:18: UP006 [*] Use `list` instead of `List` for type annotation + | +34 | def f(x: int) -> List[int]: + | ^^^^ UP006 +35 | y = List[int]() +36 | y.append(x) + | + = help: Replace with `list` + +ℹ Fix +31 31 | return cls(x=0, y=0) +32 32 | +33 33 | +34 |-def f(x: int) -> List[int]: + 34 |+def f(x: int) -> list[int]: +35 35 | y = List[int]() +36 36 | y.append(x) +37 37 | return y + +future_annotations.py:35:9: UP006 [*] Use `list` instead of `List` for type annotation + | +34 | def f(x: int) -> List[int]: +35 | y = List[int]() + | ^^^^ UP006 +36 | y.append(x) +37 | return y + | + = help: Replace with `list` + +ℹ Fix +32 32 | +33 33 | +34 34 | def f(x: int) -> List[int]: +35 |- y = List[int]() + 35 |+ y = list[int]() +36 36 | y.append(x) +37 37 | return y +38 38 | + +future_annotations.py:42:27: UP006 [*] Use `list` instead of `List` for type annotation + | +40 | x: Optional[int] = None +41 | +42 | MyList: TypeAlias = Union[List[int], List[str]] + | ^^^^ UP006 + | + = help: Replace with `list` + +ℹ Fix +39 39 | +40 40 | x: Optional[int] = None +41 41 | +42 |-MyList: TypeAlias = Union[List[int], List[str]] + 42 |+MyList: TypeAlias = Union[list[int], List[str]] + +future_annotations.py:42:38: UP006 [*] Use `list` instead of `List` for type annotation + | +40 | x: Optional[int] = None +41 | +42 | MyList: TypeAlias = Union[List[int], List[str]] + | ^^^^ UP006 + | + = help: Replace with `list` + +ℹ Fix +39 39 | +40 40 | x: Optional[int] = None +41 41 | +42 |-MyList: TypeAlias = Union[List[int], List[str]] + 42 |+MyList: TypeAlias = Union[List[int], list[str]] + + diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p37.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p37.snap new file mode 100644 index 0000000000..870ad3bf5d --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p37.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +--- + diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index f0aa41c570..a110328a24 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -30,6 +30,10 @@ mod tests { #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))] #[test_case(Rule::PairwiseOverZipped, Path::new("RUF007.py"))] #[test_case(Rule::StaticKeyDictComprehension, Path::new("RUF011.py"))] + #[cfg_attr( + feature = "unreachable-code", + test_case(Rule::UnreachableCode, Path::new("RUF014.py")) + )] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs index 367c4ee026..2a9c25725b 100644 --- a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -86,7 +86,13 @@ enum Type { /// Recursively merge all the tuples and lists in the expression. fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> { - let Expr::BinOp(ast::ExprBinOp { left, op: Operator::Add, right, range: _ }) = expr else { + let Expr::BinOp(ast::ExprBinOp { + left, + op: Operator::Add, + right, + range: _, + }) = expr + else { return None; }; @@ -171,7 +177,7 @@ pub(crate) fn collection_literal_concatenation(checker: &mut Checker, expr: &Exp } let Some((new_expr, type_)) = concatenate_expressions(expr) else { - return + return; }; let contents = match type_ { diff --git a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index b87135e0bd..419fd6df41 100644 --- a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -82,7 +82,8 @@ pub(crate) fn explicit_f_string_type_conversion( args, keywords, .. - }) = value.as_ref() else { + }) = value.as_ref() + else { continue; }; diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index 2ceb04365d..64fb85c073 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -191,7 +191,7 @@ impl<'a> TypingTarget<'a> { if semantic.match_typing_expr(value, "Optional") { return Some(TypingTarget::Optional); } - let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else{ + let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else { return None; }; if semantic.match_typing_expr(value, "Literal") { @@ -266,31 +266,41 @@ impl<'a> TypingTarget<'a> { | TypingTarget::Any | TypingTarget::Object => true, TypingTarget::Literal(elements) => elements.iter().any(|element| { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator, target_version) else { + let Some(new_target) = + TypingTarget::try_from_expr(element, semantic, locator, target_version) + else { return false; }; // Literal can only contain `None`, a literal value, other `Literal` // or an enum value. match new_target { TypingTarget::None => true, - TypingTarget::Literal(_) => new_target.contains_none(semantic, locator, target_version), + TypingTarget::Literal(_) => { + new_target.contains_none(semantic, locator, target_version) + } _ => false, } }), TypingTarget::Union(elements) => elements.iter().any(|element| { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator, target_version) else { + let Some(new_target) = + TypingTarget::try_from_expr(element, semantic, locator, target_version) + else { return false; }; new_target.contains_none(semantic, locator, target_version) }), TypingTarget::Annotated(element) => { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator, target_version) else { + let Some(new_target) = + TypingTarget::try_from_expr(element, semantic, locator, target_version) + else { return false; }; new_target.contains_none(semantic, locator, target_version) } TypingTarget::ForwardReference(expr) => { - let Some(new_target) = TypingTarget::try_from_expr(expr, semantic, locator, target_version) else { + let Some(new_target) = + TypingTarget::try_from_expr(expr, semantic, locator, target_version) + else { return false; }; new_target.contains_none(semantic, locator, target_version) @@ -312,7 +322,8 @@ fn type_hint_explicitly_allows_none<'a>( locator: &Locator, target_version: PythonVersion, ) -> Option<&'a Expr> { - let Some(target) = TypingTarget::try_from_expr(annotation, semantic, locator, target_version) else { + let Some(target) = TypingTarget::try_from_expr(annotation, semantic, locator, target_version) + else { return Some(annotation); }; match target { @@ -392,14 +403,12 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { .chain(&arguments.args) .chain(&arguments.kwonlyargs) { - let Some(default) = default else { - continue - }; + let Some(default) = default else { continue }; if !is_const_none(default) { continue; } let Some(annotation) = &def.annotation else { - continue + continue; }; if let Expr::Constant(ast::ExprConstant { @@ -410,7 +419,12 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { { // Quoted annotation. if let Ok((annotation, kind)) = parse_type_annotation(string, *range, checker.locator) { - let Some(expr) = type_hint_explicitly_allows_none(&annotation, checker.semantic(), checker.locator, checker.settings.target_version) else { + let Some(expr) = type_hint_explicitly_allows_none( + &annotation, + checker.semantic(), + checker.locator, + checker.settings.target_version, + ) else { continue; }; let conversion_type = checker.settings.target_version.into(); @@ -426,7 +440,12 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { } } else { // Unquoted annotation. - let Some(expr) = type_hint_explicitly_allows_none(annotation, checker.semantic(), checker.locator, checker.settings.target_version) else { + let Some(expr) = type_hint_explicitly_allows_none( + annotation, + checker.semantic(), + checker.locator, + checker.settings.target_version, + ) else { continue; }; let conversion_type = checker.settings.target_version.into(); diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index 6ec0eda590..f79f5fafd7 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -9,6 +9,8 @@ pub(crate) use mutable_class_default::*; pub(crate) use mutable_dataclass_default::*; pub(crate) use pairwise_over_zipped::*; pub(crate) use static_key_dict_comprehension::*; +#[cfg(feature = "unreachable-code")] +pub(crate) use unreachable::*; pub(crate) use unused_noqa::*; mod ambiguous_unicode_character; @@ -24,6 +26,8 @@ mod mutable_class_default; mod mutable_dataclass_default; mod pairwise_over_zipped; mod static_key_dict_comprehension; +#[cfg(feature = "unreachable-code")] +pub(crate) mod unreachable; mod unused_noqa; #[derive(Clone, Copy)] diff --git a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs index 657a1638aa..1b3f234a78 100644 --- a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs +++ b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs @@ -67,7 +67,7 @@ fn match_slice_info(expr: &Expr) -> Option { return None; }; - let Expr::Slice(ast::ExprSlice { lower, step, .. }) = slice.as_ref() else { + let Expr::Slice(ast::ExprSlice { lower, step, .. }) = slice.as_ref() else { return None; }; diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__assert.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__assert.py.md.snap new file mode 100644 index 0000000000..17ca4671a0 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__assert.py.md.snap @@ -0,0 +1,97 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + assert True +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert True\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + assert False +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert False\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + assert True, "oops" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert True, #quot;oops#quot;\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + assert False, "oops" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert False, #quot;oops#quot;\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__async-for.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__async-for.py.md.snap new file mode 100644 index 0000000000..431c82d33c --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__async-for.py.md.snap @@ -0,0 +1,241 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + async for i in range(5): + print(i) +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(i)\n"] + block2["async for i in range(5): + print(i)\n"] + + start --> block2 + block2 -- "range(5)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + async for i in range(20): + print(i) + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["print(i)\n"] + block1["return 0\n"] + block2["async for i in range(20): + print(i) + else: + return 0\n"] + + start --> block2 + block2 -- "range(20)" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + async for i in range(10): + if i == 5: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["async for i in range(10): + if i == 5: + return 1\n"] + + start --> block3 + block3 -- "range(10)" --> block2 + block3 -- "else" --> block0 + block2 -- "i == 5" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + async for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 2\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["return 0\n"] + block4["async for i in range(111): + if i == 5: + return 1 + else: + return 0\n"] + + start --> block4 + block4 -- "range(111)" --> block2 + block4 -- "else" --> block3 + block3 --> return + block2 -- "i == 5" --> block1 + block2 -- "else" --> block4 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + async for i in range(12): + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["async for i in range(12): + continue\n"] + + start --> block2 + block2 -- "range(12)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + async for i in range(1110): + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["async for i in range(1110): + if True: + continue\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + async for i in range(13): + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["async for i in range(13): + break\n"] + + start --> block2 + block2 -- "range(13)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + async for i in range(1110): + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["async for i in range(1110): + if True: + break\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__for.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__for.py.md.snap new file mode 100644 index 0000000000..f3d3def743 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__for.py.md.snap @@ -0,0 +1,241 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + for i in range(5): + print(i) +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(i)\n"] + block2["for i in range(5): + print(i)\n"] + + start --> block2 + block2 -- "range(5)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + for i in range(20): + print(i) + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["print(i)\n"] + block1["return 0\n"] + block2["for i in range(20): + print(i) + else: + return 0\n"] + + start --> block2 + block2 -- "range(20)" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + for i in range(10): + if i == 5: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["for i in range(10): + if i == 5: + return 1\n"] + + start --> block3 + block3 -- "range(10)" --> block2 + block3 -- "else" --> block0 + block2 -- "i == 5" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 2\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["return 0\n"] + block4["for i in range(111): + if i == 5: + return 1 + else: + return 0\n"] + + start --> block4 + block4 -- "range(111)" --> block2 + block4 -- "else" --> block3 + block3 --> return + block2 -- "i == 5" --> block1 + block2 -- "else" --> block4 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + for i in range(12): + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["for i in range(12): + continue\n"] + + start --> block2 + block2 -- "range(12)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + for i in range(1110): + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["for i in range(1110): + if True: + continue\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + for i in range(13): + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["for i in range(13): + break\n"] + + start --> block2 + block2 -- "range(13)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + for i in range(1110): + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["for i in range(1110): + if True: + break\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__if.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__if.py.md.snap new file mode 100644 index 0000000000..1ab3a11c7d --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__if.py.md.snap @@ -0,0 +1,535 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + if False: + return 0 + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 0\n"] + block2["if False: + return 0\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + if True: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + if False: + return 0 + else: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if False: + return 0 + else: + return 1\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + if True: + return 1 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 0\n"] + block2["if True: + return 1 + else: + return 0\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + if False: + return 0 + else: + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 0\n"] + block2["return 1\n"] + block3["if False: + return 0 + else: + return 1\n"] + + start --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + if True: + return 1 + else: + return 0 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["return 0\n"] + block3["if True: + return 1 + else: + return 0\n"] + + start --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + if True: + if True: + return 1 + return 2 + else: + return 3 + return "unreachable2" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable2#quot;\n"] + block1["return 2\n"] + block2["return 1\n"] + block3["if True: + return 1\n"] + block4["return 3\n"] + block5["if True: + if True: + return 1 + return 2 + else: + return 3\n"] + + start --> block5 + block5 -- "True" --> block3 + block5 -- "else" --> block4 + block4 --> return + block3 -- "True" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + if False: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["if False: + return 0\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 8 +### Source +```python +def func(): + if True: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 1\n"] + block2["if True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 9 +### Source +```python +def func(): + if True: + return 1 + elif False: + return 2 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 2\n"] + block2["return 0\n"] + block3["elif False: + return 2 + else: + return 0\n"] + block4["if True: + return 1 + elif False: + return 2 + else: + return 0\n"] + + start --> block4 + block4 -- "True" --> block0 + block4 -- "else" --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 10 +### Source +```python +def func(): + if False: + return 1 + elif True: + return 2 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 2\n"] + block2["return 0\n"] + block3["elif True: + return 2 + else: + return 0\n"] + block4["if False: + return 1 + elif True: + return 2 + else: + return 0\n"] + + start --> block4 + block4 -- "False" --> block0 + block4 -- "else" --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 11 +### Source +```python +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 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 6\n"] + block1["return 3\n"] + block2["return 0\n"] + block3["return 1\n"] + block4["return 2\n"] + block5["elif True: + return 1 + else: + return 2\n"] + block6["if False: + return 0 + elif True: + return 1 + else: + return 2\n"] + block7["return 4\n"] + block8["return 5\n"] + block9["elif True: + return 4 + else: + return 5\n"] + block10["if True: + if False: + return 0 + elif True: + return 1 + else: + return 2 + return 3 + elif True: + return 4 + else: + return 5\n"] + + start --> block10 + block10 -- "True" --> block6 + block10 -- "else" --> block9 + block9 -- "True" --> block7 + block9 -- "else" --> block8 + block8 --> return + block7 --> return + block6 -- "False" --> block2 + block6 -- "else" --> block5 + block5 -- "True" --> block3 + block5 -- "else" --> block4 + block4 --> return + block3 --> return + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 12 +### Source +```python +def func(): + if False: + return "unreached" + elif False: + return "also unreached" + return "reached" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;reached#quot;\n"] + block1["return #quot;unreached#quot;\n"] + block2["return #quot;also unreached#quot;\n"] + block3["elif False: + return #quot;also unreached#quot;\n"] + block4["if False: + return #quot;unreached#quot; + elif False: + return #quot;also unreached#quot;\n"] + + start --> block4 + block4 -- "False" --> block1 + block4 -- "else" --> block3 + block3 -- "False" --> block2 + block3 -- "else" --> block0 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 13 +### Source +```python +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 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return buffer.data\n"] + block1["return base64.b64decode(data)\n"] + block2["buffer = data\n"] + block3["buffer = self._buffers[id]\n"] + block4["self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block5["id = data[#quot;id#quot;]\nif id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block6["elif isinstance(data, Buffer): + buffer = data + else: + id = data[#quot;id#quot;] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block7["data = obj[#quot;data#quot;]\nif isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data[#quot;id#quot;] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + + start --> block7 + block7 -- "isinstance(data, str)" --> block1 + block7 -- "else" --> block6 + block6 -- "isinstance(data, Buffer)" --> block2 + block6 -- "else" --> block5 + block5 -- "id in self._buffers" --> block3 + block5 -- "else" --> block4 + block4 --> block0 + block3 --> block0 + block2 --> block0 + block1 --> return + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__match.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__match.py.md.snap new file mode 100644 index 0000000000..d8a6ddb59b --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__match.py.md.snap @@ -0,0 +1,776 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(status): + match status: + case _: + return 0 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 0\n"] + block2["match status: + case _: + return 0\n"] + + start --> block2 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(status): + match status: + case 1: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["match status: + case 1: + return 1\n"] + + start --> block2 + block2 -- "case 1" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(status): + match status: + case 1: + return 1 + case _: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["match status: + case 1: + return 1 + case _: + return 0\n"] + block2["return 1\n"] + block3["match status: + case 1: + return 1 + case _: + return 0\n"] + + start --> block3 + block3 -- "case 1" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> block0 + block0 --> return +``` + +## Function 3 +### Source +```python +def func(status): + match status: + case 1 | 2 | 3: + return 5 + return 6 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 6\n"] + block1["return 5\n"] + block2["match status: + case 1 | 2 | 3: + return 5\n"] + + start --> block2 + block2 -- "case 1 | 2 | 3" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(status): + match status: + case 1 | 2 | 3: + return 5 + case _: + return 10 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 10\n"] + block2["match status: + case 1 | 2 | 3: + return 5 + case _: + return 10\n"] + block3["return 5\n"] + block4["match status: + case 1 | 2 | 3: + return 5 + case _: + return 10\n"] + + start --> block4 + block4 -- "case 1 | 2 | 3" --> block3 + block4 -- "else" --> block2 + block3 --> return + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(status): + match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return "1 again" + case _: + return 3 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 3\n"] + block1["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block2["return #quot;1 again#quot;\n"] + block3["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block4["return 1\n"] + block5["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block6["return 0\n"] + block7["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + + start --> block7 + block7 -- "case 0" --> block6 + block7 -- "else" --> block5 + block6 --> return + block5 -- "case 1" --> block4 + block5 -- "else" --> block3 + block4 --> return + block3 -- "case 1" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> block0 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(status): + i = 0 + match status, i: + case _, _: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["match status, i: + case _, _: + return 0\n"] + block3["i = 0\n"] + + start --> block3 + block3 --> block2 + block2 -- "case _, _" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 7 +### Source +```python +def func(status): + i = 0 + match status, i: + case _, 0: + return 0 + case _, 2: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["match status, i: + case _, 0: + return 0 + case _, 2: + return 0\n"] + block3["return 0\n"] + block4["match status, i: + case _, 0: + return 0 + case _, 2: + return 0\n"] + block5["i = 0\n"] + + start --> block5 + block5 --> block4 + block4 -- "case _, 0" --> block3 + block4 -- "else" --> block2 + block3 --> return + block2 -- "case _, 2" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 8 +### Source +```python +def func(point): + match point: + case (0, 0): + print("Origin") + case _: + raise ValueError("oops") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["raise ValueError(#quot;oops#quot;)\n"] + block2["match point: + case (0, 0): + print(#quot;Origin#quot;) + case _: + raise ValueError(#quot;oops#quot;)\n"] + block3["print(#quot;Origin#quot;)\n"] + block4["match point: + case (0, 0): + print(#quot;Origin#quot;) + case _: + raise ValueError(#quot;oops#quot;)\n"] + + start --> block4 + block4 -- "case (0, 0)" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 9 +### Source +```python +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") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["raise ValueError(#quot;Not a point#quot;)\n"] + block2["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block3["print(f#quot;X={x}, Y={y}#quot;)\n"] + block4["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block5["print(f#quot;X={x}#quot;)\n"] + block6["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block7["print(f#quot;Y={y}#quot;)\n"] + block8["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block9["print(#quot;Origin#quot;)\n"] + block10["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + + start --> block10 + block10 -- "case (0, 0)" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case (0, y)" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case (x, 0)" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case (x, y)" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 10 +### Source +```python +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") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;Not a point#quot;)\n"] + block2["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block3["print(#quot;Somewhere else#quot;)\n"] + block4["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block5["print(f#quot;X={x}#quot;)\n"] + block6["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block7["print(f#quot;Y={y}#quot;)\n"] + block8["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block9["print(#quot;Origin#quot;)\n"] + block10["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block11["class Point: + x: int + y: int\n"] + + start --> block11 + block11 --> block10 + block10 -- "case Point(x=0, y=0)" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case Point(x=0, y=y)" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case Point(x=x, y=0)" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case Point()" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> block0 + block0 --> return +``` + +## Function 11 +### Source +```python +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") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;Something else#quot;)\n"] + block2["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block3["print(f#quot;Two on the Y axis at {y1}, {y2}#quot;)\n"] + block4["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block5["print(f#quot;Single point {x}, {y}#quot;)\n"] + block6["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block7["print(#quot;The origin#quot;)\n"] + block8["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block9["print(#quot;No points#quot;)\n"] + block10["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + + start --> block10 + block10 -- "case []" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case [Point(0, 0)]" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case [Point(x, y)]" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case [Point(0, y1), Point(0, y2)]" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> block0 + block0 --> return +``` + +## Function 12 +### Source +```python +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") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(f#quot;Not on the diagonal#quot;)\n"] + block2["match point: + case Point(x, y) if x == y: + print(f#quot;Y=X at {x}#quot;) + case Point(x, y): + print(f#quot;Not on the diagonal#quot;)\n"] + block3["print(f#quot;Y=X at {x}#quot;)\n"] + block4["match point: + case Point(x, y) if x == y: + print(f#quot;Y=X at {x}#quot;) + case Point(x, y): + print(f#quot;Not on the diagonal#quot;)\n"] + + start --> block4 + block4 -- "case Point(x, y) if x == y" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 -- "case Point(x, y)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 13 +### Source +```python +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 :(") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;I'm feeling the blues :(#quot;)\n"] + block2["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block3["print(#quot;Grass is green#quot;)\n"] + block4["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block5["print(#quot;I see red!#quot;)\n"] + block6["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block7["from enum import Enum\nclass Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue'\ncolor = Color(input(#quot;Enter your choice of 'red', 'blue' or 'green': #quot;))\n"] + + start --> block7 + block7 --> block6 + block6 -- "case Color.RED" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case Color.GREEN" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 -- "case Color.BLUE" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__raise.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__raise.py.md.snap new file mode 100644 index 0000000000..7da998458d --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__raise.py.md.snap @@ -0,0 +1,41 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + raise Exception +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["raise Exception\n"] + + start --> block0 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + raise "a glass!" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["raise #quot;a glass!#quot;\n"] + + start --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__simple.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__simple.py.md.snap new file mode 100644 index 0000000000..881df6fad1 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__simple.py.md.snap @@ -0,0 +1,136 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + pass +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["pass\n"] + + start --> block0 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + pass +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["pass\n"] + + start --> block0 + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + return +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return\n"] + + start --> block0 + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + + start --> block0 + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + + start --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + i = 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["i = 0\n"] + + start --> block0 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + i = 0 + i += 2 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["i = 0\ni += 2\nreturn i\n"] + + start --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__while.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__while.py.md.snap new file mode 100644 index 0000000000..aa030a03a4 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__while.py.md.snap @@ -0,0 +1,527 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + while False: + return "unreachable" + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return #quot;unreachable#quot;\n"] + block2["while False: + return #quot;unreachable#quot;\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + while False: + return "unreachable" + else: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["while False: + return #quot;unreachable#quot; + else: + return 1\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + while False: + return "unreachable" + else: + return 1 + return "also unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;also unreachable#quot;\n"] + block1["return #quot;unreachable#quot;\n"] + block2["return 1\n"] + block3["while False: + return #quot;unreachable#quot; + else: + return 1\n"] + + start --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + while True: + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["while True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + while True: + return 1 + else: + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return #quot;unreachable#quot;\n"] + block2["while True: + return 1 + else: + return #quot;unreachable#quot;\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + while True: + return 1 + else: + return "unreachable" + return "also unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;also unreachable#quot;\n"] + block1["return 1\n"] + block2["return #quot;unreachable#quot;\n"] + block3["while True: + return 1 + else: + return #quot;unreachable#quot;\n"] + + start --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + i = 0 + while False: + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["i = 0\nwhile False: + i += 1\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + i = 0 + while True: + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["i = 0\nwhile True: + i += 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 8 +### Source +```python +def func(): + while True: + pass + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["pass\n"] + block2["while True: + pass\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 9 +### Source +```python +def func(): + i = 0 + while True: + if True: + print("ok") + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["print(#quot;ok#quot;)\n"] + block3["if True: + print(#quot;ok#quot;)\n"] + block4["i = 0\nwhile True: + if True: + print(#quot;ok#quot;) + i += 1\n"] + + start --> block4 + block4 -- "True" --> block3 + block4 -- "else" --> block0 + block3 -- "True" --> block2 + block3 -- "else" --> block1 + block2 --> block1 + block1 --> block4 + block0 --> return +``` + +## Function 10 +### Source +```python +def func(): + i = 0 + while True: + if False: + print("ok") + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["print(#quot;ok#quot;)\n"] + block3["if False: + print(#quot;ok#quot;)\n"] + block4["i = 0\nwhile True: + if False: + print(#quot;ok#quot;) + i += 1\n"] + + start --> block4 + block4 -- "True" --> block3 + block4 -- "else" --> block0 + block3 -- "False" --> block2 + block3 -- "else" --> block1 + block2 --> block1 + block1 --> block4 + block0 --> return +``` + +## Function 11 +### Source +```python +def func(): + while True: + if True: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if True: + return 1\n"] + block3["while True: + if True: + return 1\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 12 +### Source +```python +def func(): + while True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["while True: + continue\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 13 +### Source +```python +def func(): + while False: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["while False: + continue\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 14 +### Source +```python +def func(): + while True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["while True: + break\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 15 +### Source +```python +def func(): + while False: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["while False: + break\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 16 +### Source +```python +def func(): + while True: + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["while True: + if True: + continue\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 17 +### Source +```python +def func(): + while True: + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["while True: + if True: + break\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/unreachable.rs b/crates/ruff/src/rules/ruff/rules/unreachable.rs new file mode 100644 index 0000000000..8aede6a613 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/unreachable.rs @@ -0,0 +1,1101 @@ +use std::{fmt, iter, usize}; + +use log::error; +use rustpython_parser::ast::{ + Expr, Identifier, MatchCase, Pattern, PatternMatchAs, Ranged, Stmt, StmtAsyncFor, + StmtAsyncWith, StmtFor, StmtMatch, StmtReturn, StmtTry, StmtTryStar, StmtWhile, StmtWith, +}; +use rustpython_parser::text_size::{TextRange, TextSize}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_index::{IndexSlice, IndexVec}; +use ruff_macros::{derive_message_formats, newtype_index, violation}; + +/// ## What it does +/// Checks for unreachable code. +/// +/// ## Why is this bad? +/// Unreachable code can be a maintenance burden without ever being used. +/// +/// ## Example +/// ```python +/// def function(): +/// if False: +/// return "unreachable" +/// return "reachable" +/// ``` +/// +/// Use instead: +/// ```python +/// def function(): +/// return "reachable" +/// ``` +#[violation] +pub struct UnreachableCode { + name: String, +} + +impl Violation for UnreachableCode { + #[derive_message_formats] + fn message(&self) -> String { + let UnreachableCode { name } = self; + format!("Unreachable code in {name}") + } +} + +pub(crate) fn in_function(name: &Identifier, body: &[Stmt]) -> Vec { + // Create basic code blocks from the body. + let basic_blocks = BasicBlocks::from(body); + + // Basic on the code blocks we can (more) easily follow what statements are + // and aren't reached, we'll mark them as such in `reached_map`. + let mut reached_map = Bitmap::with_capacity(basic_blocks.len()); + + if let Some(start_index) = basic_blocks.start_index() { + mark_reached(&mut reached_map, &basic_blocks.blocks, start_index); + } + + // For each unreached code block create a diagnostic. + reached_map + .unset() + .filter_map(|idx| { + let block = &basic_blocks.blocks[idx]; + if block.is_sentinel() { + return None; + } + + // TODO: add more information to the diagnostic. Include the entire + // code block, not just the first line. Maybe something to indicate + // the code flow and where it prevents this block from being reached + // for example. + let Some(stmt) = block.stmts.first() else { + // This should never happen. + error!("Got an unexpected empty code block"); + return None; + }; + Some(Diagnostic::new( + UnreachableCode { + name: name.as_str().to_owned(), + }, + stmt.range(), + )) + }) + .collect() +} + +/// Simple bitmap. +#[derive(Debug)] +struct Bitmap { + bits: Box<[usize]>, + capacity: usize, +} + +impl Bitmap { + /// Create a new `Bitmap` with `capacity` capacity. + fn with_capacity(capacity: usize) -> Bitmap { + let mut size = capacity / usize::BITS as usize; + if (capacity % usize::BITS as usize) != 0 { + size += 1; + } + Bitmap { + bits: vec![0; size].into_boxed_slice(), + capacity, + } + } + + /// Set bit at index `idx` to true. + /// + /// Returns a boolean indicating if the bit was already set. + fn set(&mut self, idx: BlockIndex) -> bool { + let bits_index = (idx.as_u32() / usize::BITS) as usize; + let shift = idx.as_u32() % usize::BITS; + if (self.bits[bits_index] & (1 << shift)) == 0 { + self.bits[bits_index] |= 1 << shift; + false + } else { + true + } + } + + /// Returns an iterator of all unset indices. + fn unset(&self) -> impl Iterator + '_ { + let mut index = 0; + let mut shift = 0; + let last_max_shift = self.capacity % usize::BITS as usize; + iter::from_fn(move || loop { + if shift >= usize::BITS as usize { + shift = 0; + index += 1; + } + if self.bits.len() <= index || (index >= self.bits.len() - 1 && shift >= last_max_shift) + { + return None; + } + + let is_set = (self.bits[index] & (1 << shift)) != 0; + shift += 1; + if !is_set { + return Some(BlockIndex::from_usize( + (index * usize::BITS as usize) + shift - 1, + )); + } + }) + } +} + +/// Set bits in `reached_map` for all blocks that are reached in `blocks` +/// starting with block at index `idx`. +fn mark_reached( + reached_map: &mut Bitmap, + blocks: &IndexSlice>, + start_index: BlockIndex, +) { + let mut idx = start_index; + + loop { + let block = &blocks[idx]; + if reached_map.set(idx) { + return; // Block already visited, no needed to do it again. + } + + match &block.next { + NextBlock::Always(next) => idx = *next, + NextBlock::If { + condition, + next, + orelse, + } => { + match taken(condition) { + Some(true) => idx = *next, // Always taken. + Some(false) => idx = *orelse, // Never taken. + None => { + // Don't know, both branches might be taken. + idx = *next; + mark_reached(reached_map, blocks, *orelse); + } + } + } + NextBlock::Terminate => return, + } + } +} + +/// Determines if `condition` is taken. +/// Returns `Some(true)` if the condition is always true, e.g. `if True`, same +/// with `Some(false)` if it's never taken. If it can't be determined it returns +/// `None`, e.g. `If i == 100`. +fn taken(condition: &Condition) -> Option { + // TODO: add more cases to this where we can determine a condition + // statically. For now we only consider constant booleans. + match condition { + Condition::Test(expr) => match expr { + Expr::Constant(constant) => constant.value.as_bool().copied(), + _ => None, + }, + Condition::Iterator(_) => None, + Condition::Match { .. } => None, + } +} + +/// Index into [`BasicBlocks::blocks`]. +#[newtype_index] +#[derive(PartialOrd, Ord)] +struct BlockIndex; + +/// Collection of basic block. +#[derive(Debug, PartialEq)] +struct BasicBlocks<'stmt> { + /// # Notes + /// + /// The order of these block is unspecified. However it's guaranteed that + /// the last block is the first statement in the function and the first + /// block is the last statement. The block are more or less in reverse + /// order, but it gets fussy around control flow statements (e.g. `while` + /// statements). + /// + /// For loop blocks, and similar recurring control flows, the end of the + /// body will point to the loop block again (to create the loop). However an + /// oddity here is that this block might contain statements before the loop + /// itself which, of course, won't be executed again. + /// + /// For example: + /// ```python + /// i = 0 # block 0 + /// while True: # + /// continue # block 1 + /// ``` + /// Will create a connection between block 1 (loop body) and block 0, which + /// includes the `i = 0` statement. + /// + /// To keep `NextBlock` simple(r) `NextBlock::If`'s `next` and `orelse` + /// fields only use `BlockIndex`, which means that they can't terminate + /// themselves. To support this we insert *empty*/fake blocks before the end + /// of the function that we can link to. + /// + /// Finally `BasicBlock` can also be a sentinel node, see the associated + /// constants of [`BasicBlock`]. + blocks: IndexVec>, +} + +impl BasicBlocks<'_> { + fn len(&self) -> usize { + self.blocks.len() + } + + fn start_index(&self) -> Option { + self.blocks.indices().last() + } +} + +impl<'stmt> From<&'stmt [Stmt]> for BasicBlocks<'stmt> { + /// # Notes + /// + /// This assumes that `stmts` is a function body. + fn from(stmts: &'stmt [Stmt]) -> BasicBlocks<'stmt> { + let mut blocks = BasicBlocksBuilder::with_capacity(stmts.len()); + + blocks.create_blocks(stmts, None); + + blocks.finish() + } +} + +/// Basic code block, sequence of statements unconditionally executed +/// "together". +#[derive(Debug, PartialEq)] +struct BasicBlock<'stmt> { + stmts: &'stmt [Stmt], + next: NextBlock<'stmt>, +} + +/// Edge between basic blocks (in the control-flow graph). +#[derive(Debug, PartialEq)] +enum NextBlock<'stmt> { + /// Always continue with a block. + Always(BlockIndex), + /// Condition jump. + If { + /// Condition that needs to be evaluated to jump to the `next` or + /// `orelse` block. + condition: Condition<'stmt>, + /// Next block if `condition` is true. + next: BlockIndex, + /// Next block if `condition` is false. + orelse: BlockIndex, + }, + /// The end. + Terminate, +} + +/// Condition used to determine to take the `next` or `orelse` branch in +/// [`NextBlock::If`]. +#[derive(Clone, Debug, PartialEq)] +enum Condition<'stmt> { + /// Conditional statement, this should evaluate to a boolean, for e.g. `if` + /// or `while`. + Test(&'stmt Expr), + /// Iterator for `for` statements, e.g. for `i in range(10)` this will be + /// `range(10)`. + Iterator(&'stmt Expr), + Match { + /// `match $subject`. + subject: &'stmt Expr, + /// `case $case`, include pattern, guard, etc. + case: &'stmt MatchCase, + }, +} + +impl<'stmt> Ranged for Condition<'stmt> { + fn range(&self) -> TextRange { + match self { + Condition::Test(expr) | Condition::Iterator(expr) => expr.range(), + // The case of the match statement, without the body. + Condition::Match { subject: _, case } => TextRange::new( + case.start(), + case.guard + .as_ref() + .map_or(case.pattern.end(), |guard| guard.end()), + ), + } + } +} + +impl<'stmt> BasicBlock<'stmt> { + /// A sentinel block indicating an empty termination block. + const EMPTY: BasicBlock<'static> = BasicBlock { + stmts: &[], + next: NextBlock::Terminate, + }; + + /// A sentinel block indicating an exception was raised. + const EXCEPTION: BasicBlock<'static> = BasicBlock { + stmts: &[Stmt::Return(StmtReturn { + range: TextRange::new(TextSize::new(0), TextSize::new(0)), + value: None, + })], + next: NextBlock::Terminate, + }; + + /// Return true if the block is a sentinel or fake block. + fn is_sentinel(&self) -> bool { + self.is_empty() || self.is_exception() + } + + /// Returns an empty block that terminates. + fn is_empty(&self) -> bool { + matches!(self.next, NextBlock::Terminate) && self.stmts.is_empty() + } + + /// Returns true if `self` an [`BasicBlock::EXCEPTION`]. + fn is_exception(&self) -> bool { + matches!(self.next, NextBlock::Terminate) && BasicBlock::EXCEPTION.stmts == self.stmts + } +} + +/// Handle a loop block, such as a `while`, `for` or `async for` statement. +fn loop_block<'stmt>( + blocks: &mut BasicBlocksBuilder<'stmt>, + condition: Condition<'stmt>, + body: &'stmt [Stmt], + orelse: &'stmt [Stmt], + after: Option, +) -> NextBlock<'stmt> { + let after_block = blocks.maybe_next_block_index(after, || orelse.is_empty()); + // NOTE: a while loop's body must not be empty, so we can safely + // create at least one block from it. + let last_statement_index = blocks.append_blocks(body, after); + let last_orelse_statement = blocks.append_blocks_if_not_empty(orelse, after_block); + // `create_blocks` always continues to the next block by + // default. However in a while loop we want to continue with the + // while block (we're about to create) to create the loop. + // NOTE: `blocks.len()` is an invalid index at time of creation + // as it points to the block which we're about to create. + blocks.change_next_block( + last_statement_index, + after_block, + blocks.blocks.next_index(), + |block| { + // For `break` statements we don't want to continue with the + // loop, but instead with the statement after the loop (i.e. + // not change anything). + !block.stmts.last().map_or(false, Stmt::is_break_stmt) + }, + ); + NextBlock::If { + condition, + next: last_statement_index, + orelse: last_orelse_statement, + } +} + +/// Handle a single match case. +/// +/// `next_after_block` is the block *after* the entire match statement that is +/// taken after this case is taken. +/// `orelse_after_block` is the next match case (or the block after the match +/// statement if this is the last case). +fn match_case<'stmt>( + blocks: &mut BasicBlocksBuilder<'stmt>, + match_stmt: &'stmt Stmt, + subject: &'stmt Expr, + case: &'stmt MatchCase, + next_after_block: BlockIndex, + orelse_after_block: BlockIndex, +) -> BasicBlock<'stmt> { + // FIXME: this is not ideal, we want to only use the `case` statement here, + // but that is type `MatchCase`, not `Stmt`. For now we'll point to the + // entire match statement. + let stmts = std::slice::from_ref(match_stmt); + let next_block_index = if case.body.is_empty() { + next_after_block + } else { + let from = blocks.last_index(); + let last_statement_index = blocks.append_blocks(&case.body, Some(next_after_block)); + if let Some(from) = from { + blocks.change_next_block(last_statement_index, from, next_after_block, |_| true); + } + last_statement_index + }; + // TODO: handle named arguments, e.g. + // ```python + // match $subject: + // case $binding: + // print($binding) + // ``` + // These should also return `NextBlock::Always`. + let next = if is_wildcard(case) { + // Wildcard case is always taken. + NextBlock::Always(next_block_index) + } else { + NextBlock::If { + condition: Condition::Match { subject, case }, + next: next_block_index, + orelse: orelse_after_block, + } + }; + BasicBlock { stmts, next } +} + +/// Returns true if `pattern` is a wildcard (`_`) pattern. +fn is_wildcard(pattern: &MatchCase) -> bool { + pattern.guard.is_none() + && matches!(&pattern.pattern, Pattern::MatchAs(PatternMatchAs { pattern, name, .. }) if pattern.is_none() && name.is_none()) +} + +#[derive(Debug, Default)] +struct BasicBlocksBuilder<'stmt> { + blocks: IndexVec>, +} + +impl<'stmt> BasicBlocksBuilder<'stmt> { + fn with_capacity(capacity: usize) -> Self { + Self { + blocks: IndexVec::with_capacity(capacity), + } + } + + /// Creates basic blocks from `stmts` and appends them to `blocks`. + fn create_blocks( + &mut self, + stmts: &'stmt [Stmt], + mut after: Option, + ) -> Option { + // We process the statements in reverse so that we can always point to the + // next block (as that should always be processed). + let mut stmts_iter = stmts.iter().enumerate().rev().peekable(); + while let Some((i, stmt)) = stmts_iter.next() { + let next = match stmt { + // Statements that continue to the next statement after execution. + Stmt::FunctionDef(_) + | Stmt::AsyncFunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Break(_) + | Stmt::Pass(_) => self.unconditional_next_block(after), + Stmt::Continue(_) => { + // NOTE: the next branch gets fixed up in `change_next_block`. + self.unconditional_next_block(after) + } + // Statements that (can) divert the control flow. + Stmt::If(stmt) => { + let next_after_block = + self.maybe_next_block_index(after, || needs_next_block(&stmt.body)); + let orelse_after_block = + self.maybe_next_block_index(after, || needs_next_block(&stmt.orelse)); + let next = self.append_blocks_if_not_empty(&stmt.body, next_after_block); + let orelse = self.append_blocks_if_not_empty(&stmt.orelse, orelse_after_block); + NextBlock::If { + condition: Condition::Test(&stmt.test), + next, + orelse, + } + } + Stmt::While(StmtWhile { + test: condition, + body, + orelse, + .. + }) => loop_block(self, Condition::Test(condition), body, orelse, after), + Stmt::For(StmtFor { + iter: condition, + body, + orelse, + .. + }) + | Stmt::AsyncFor(StmtAsyncFor { + iter: condition, + body, + orelse, + .. + }) => loop_block(self, Condition::Iterator(condition), body, orelse, after), + Stmt::Try(StmtTry { + body, + handlers, + orelse, + finalbody, + .. + }) + | Stmt::TryStar(StmtTryStar { + body, + handlers, + orelse, + finalbody, + .. + }) => { + // TODO: handle `try` statements. The `try` control flow is very + // complex, what blocks are and aren't taken and from which + // block the control flow is actually returns is **very** + // specific to the contents of the block. Read + // + // very carefully. + // For now we'll skip over it. + let _ = (body, handlers, orelse, finalbody); // Silence unused code warnings. + self.unconditional_next_block(after) + } + Stmt::With(StmtWith { + items, + body, + type_comment, + .. + }) + | Stmt::AsyncWith(StmtAsyncWith { + items, + body, + type_comment, + .. + }) => { + // TODO: handle `with` statements, see + // . + // I recommend to `try` statements first as `with` can desugar + // to a `try` statement. + // For now we'll skip over it. + let _ = (items, body, type_comment); // Silence unused code warnings. + self.unconditional_next_block(after) + } + Stmt::Match(StmtMatch { subject, cases, .. }) => { + let next_after_block = self.maybe_next_block_index(after, || { + // We don't need need a next block if all cases don't need a + // next block, i.e. if no cases need a next block, and we + // have a wildcard case (to ensure one of the block is + // always taken). + // NOTE: match statement require at least one case, so we + // don't have to worry about empty `cases`. + // TODO: support exhaustive cases without a wildcard. + cases.iter().any(|case| needs_next_block(&case.body)) + || !cases.iter().any(is_wildcard) + }); + let mut orelse_after_block = next_after_block; + for case in cases.iter().rev() { + let block = match_case( + self, + stmt, + subject, + case, + next_after_block, + orelse_after_block, + ); + // For the case above this use the just added case as the + // `orelse` branch, this convert the match statement to + // (essentially) a bunch of if statements. + orelse_after_block = self.blocks.push(block); + } + // TODO: currently we don't include the lines before the match + // statement in the block, unlike what we do for other + // statements. + after = Some(orelse_after_block); + continue; + } + Stmt::Raise(_) => { + // TODO: this needs special handling within `try` and `with` + // statements. For now we just terminate the execution, it's + // possible it's continued in an `catch` or `finally` block, + // possibly outside of the function. + // Also see `Stmt::Assert` handling. + NextBlock::Terminate + } + Stmt::Assert(stmt) => { + // TODO: this needs special handling within `try` and `with` + // statements. For now we just terminate the execution if the + // assertion fails, it's possible it's continued in an `catch` + // or `finally` block, possibly outside of the function. + // Also see `Stmt::Raise` handling. + let next = self.force_next_block_index(); + let orelse = self.fake_exception_block_index(); + NextBlock::If { + condition: Condition::Test(&stmt.test), + next, + orelse, + } + } + Stmt::Expr(stmt) => { + match &*stmt.value { + Expr::BoolOp(_) + | Expr::BinOp(_) + | Expr::UnaryOp(_) + | Expr::Dict(_) + | Expr::Set(_) + | Expr::Compare(_) + | Expr::Call(_) + | Expr::FormattedValue(_) + | Expr::JoinedStr(_) + | Expr::Constant(_) + | Expr::Attribute(_) + | Expr::Subscript(_) + | Expr::Starred(_) + | Expr::Name(_) + | Expr::List(_) + | Expr::Tuple(_) + | Expr::Slice(_) => self.unconditional_next_block(after), + // TODO: handle these expressions. + Expr::NamedExpr(_) + | Expr::Lambda(_) + | Expr::IfExp(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) + | Expr::GeneratorExp(_) + | Expr::Await(_) + | Expr::Yield(_) + | Expr::YieldFrom(_) => self.unconditional_next_block(after), + } + } + // The tough branches are done, here is an easy one. + Stmt::Return(_) => NextBlock::Terminate, + }; + + // Include any statements in the block that don't divert the control flow. + let mut start = i; + let end = i + 1; + while stmts_iter + .next_if(|(_, stmt)| !is_control_flow_stmt(stmt)) + .is_some() + { + start -= 1; + } + + let block = BasicBlock { + stmts: &stmts[start..end], + next, + }; + after = Some(self.blocks.push(block)); + } + + after + } + + /// Calls [`create_blocks`] and returns this first block reached (i.e. the last + /// block). + fn append_blocks(&mut self, stmts: &'stmt [Stmt], after: Option) -> BlockIndex { + assert!(!stmts.is_empty()); + self.create_blocks(stmts, after) + .expect("Expect `create_blocks` to create a block if `stmts` is not empty") + } + + /// If `stmts` is not empty this calls [`create_blocks`] and returns this first + /// block reached (i.e. the last block). If `stmts` is empty this returns + /// `after` and doesn't change `blocks`. + fn append_blocks_if_not_empty( + &mut self, + stmts: &'stmt [Stmt], + after: BlockIndex, + ) -> BlockIndex { + if stmts.is_empty() { + after // Empty body, continue with block `after` it. + } else { + self.append_blocks(stmts, Some(after)) + } + } + + /// Select the next block from `blocks` unconditonally. + fn unconditional_next_block(&self, after: Option) -> NextBlock<'static> { + if let Some(after) = after { + return NextBlock::Always(after); + } + + // Either we continue with the next block (that is the last block `blocks`). + // Or it's the last statement, thus we terminate. + self.blocks + .last_index() + .map_or(NextBlock::Terminate, NextBlock::Always) + } + + /// Select the next block index from `blocks`. If there is no next block it will + /// add a fake/empty block. + fn force_next_block_index(&mut self) -> BlockIndex { + self.maybe_next_block_index(None, || true) + } + + /// Select the next block index from `blocks`. If there is no next block it will + /// add a fake/empty block if `condition` returns true. If `condition` returns + /// false the returned index may not be used. + fn maybe_next_block_index( + &mut self, + after: Option, + condition: impl FnOnce() -> bool, + ) -> BlockIndex { + if let Some(after) = after { + // Next block is already determined. + after + } else if let Some(idx) = self.blocks.last_index() { + // Otherwise we either continue with the next block (that is the last + // block in `blocks`). + idx + } else if condition() { + // Or if there are no blocks, but need one based on `condition` than we + // add a fake end block. + self.blocks.push(BasicBlock::EMPTY) + } else { + // NOTE: invalid, but because `condition` returned false this shouldn't + // be used. This only used as an optimisation to avoid adding fake end + // blocks. + BlockIndex::MAX + } + } + + /// Returns a block index for a fake exception block in `blocks`. + fn fake_exception_block_index(&mut self) -> BlockIndex { + for (i, block) in self.blocks.iter_enumerated() { + if block.is_exception() { + return i; + } + } + self.blocks.push(BasicBlock::EXCEPTION) + } + + /// Change the next basic block for the block, or chain of blocks, in index + /// `fixup_index` from `from` to `to`. + /// + /// This doesn't change the target if it's `NextBlock::Terminate`. + fn change_next_block( + &mut self, + mut fixup_index: BlockIndex, + from: BlockIndex, + to: BlockIndex, + check_condition: impl Fn(&BasicBlock) -> bool + Copy, + ) { + /// Check if we found our target and if `check_condition` is met. + fn is_target( + block: &BasicBlock<'_>, + got: BlockIndex, + expected: BlockIndex, + check_condition: impl Fn(&BasicBlock) -> bool, + ) -> bool { + got == expected && check_condition(block) + } + + loop { + match self.blocks.get(fixup_index).map(|b| &b.next) { + Some(NextBlock::Always(next)) => { + let next = *next; + if is_target(&self.blocks[fixup_index], next, from, check_condition) { + // Found our target, change it. + self.blocks[fixup_index].next = NextBlock::Always(to); + } + return; + } + Some(NextBlock::If { + condition, + next, + orelse, + }) => { + let idx = fixup_index; + let condition = condition.clone(); + let next = *next; + let orelse = *orelse; + let new_next = if is_target(&self.blocks[idx], next, from, check_condition) { + // Found our target in the next branch, change it (below). + Some(to) + } else { + // Follow the chain. + fixup_index = next; + None + }; + + let new_orelse = if is_target(&self.blocks[idx], orelse, from, check_condition) + { + // Found our target in the else branch, change it (below). + Some(to) + } else if new_next.is_none() { + // If we done with the next branch we only continue with the + // else branch. + fixup_index = orelse; + None + } else { + // If we're not done with the next and else branches we need + // to deal with the else branch before deal with the next + // branch (in the next iteration). + self.change_next_block(orelse, from, to, check_condition); + None + }; + + let (next, orelse) = match (new_next, new_orelse) { + (Some(new_next), Some(new_orelse)) => (new_next, new_orelse), + (Some(new_next), None) => (new_next, orelse), + (None, Some(new_orelse)) => (next, new_orelse), + (None, None) => continue, // Not changing anything. + }; + + self.blocks[idx].next = NextBlock::If { + condition, + next, + orelse, + }; + } + Some(NextBlock::Terminate) | None => return, + } + } + } + + fn finish(mut self) -> BasicBlocks<'stmt> { + if self.blocks.is_empty() { + self.blocks.push(BasicBlock::EMPTY); + } + + BasicBlocks { + blocks: self.blocks, + } + } +} + +impl<'stmt> std::ops::Deref for BasicBlocksBuilder<'stmt> { + type Target = IndexSlice>; + + fn deref(&self) -> &Self::Target { + &self.blocks + } +} + +impl<'stmt> std::ops::DerefMut for BasicBlocksBuilder<'stmt> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.blocks + } +} + +/// Returns true if `stmts` need a next block, false otherwise. +fn needs_next_block(stmts: &[Stmt]) -> bool { + // No statements, we automatically continue with the next block. + let Some(last) = stmts.last() else { + return true; + }; + + match last { + Stmt::Return(_) | Stmt::Raise(_) => false, + Stmt::If(stmt) => needs_next_block(&stmt.body) || needs_next_block(&stmt.orelse), + Stmt::FunctionDef(_) + | Stmt::AsyncFunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Expr(_) + | Stmt::Pass(_) + // TODO: check below. + | Stmt::Break(_) + | Stmt::Continue(_) + | Stmt::For(_) + | Stmt::AsyncFor(_) + | Stmt::While(_) + | Stmt::With(_) + | Stmt::AsyncWith(_) + | Stmt::Match(_) + | Stmt::Try(_) + | Stmt::TryStar(_) + | Stmt::Assert(_) => true, + } +} + +/// Returns true if `stmt` contains a control flow statement, e.g. an `if` or +/// `return` statement. +fn is_control_flow_stmt(stmt: &Stmt) -> bool { + match stmt { + Stmt::FunctionDef(_) + | Stmt::AsyncFunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Expr(_) + | Stmt::Pass(_) => false, + Stmt::Return(_) + | Stmt::For(_) + | Stmt::AsyncFor(_) + | Stmt::While(_) + | Stmt::If(_) + | Stmt::With(_) + | Stmt::AsyncWith(_) + | Stmt::Match(_) + | Stmt::Raise(_) + | Stmt::Try(_) + | Stmt::TryStar(_) + | Stmt::Assert(_) + | Stmt::Break(_) + | Stmt::Continue(_) => true, + } +} + +/// Type to create a Mermaid graph. +/// +/// To learn amount Mermaid see , for the syntax +/// see . +struct MermaidGraph<'stmt, 'source> { + graph: &'stmt BasicBlocks<'stmt>, + source: &'source str, +} + +impl<'stmt, 'source> fmt::Display for MermaidGraph<'stmt, 'source> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Flowchart type of graph, top down. + writeln!(f, "flowchart TD")?; + + // List all blocks. + writeln!(f, " start((\"Start\"))")?; + writeln!(f, " return((\"End\"))")?; + for (i, block) in self.graph.blocks.iter().enumerate() { + let (open, close) = if block.is_sentinel() { + ("[[", "]]") + } else { + ("[", "]") + }; + write!(f, " block{i}{open}\"")?; + if block.is_empty() { + write!(f, "`*(empty)*`")?; + } else if block.is_exception() { + write!(f, "Exception raised")?; + } else { + for stmt in block.stmts { + let code_line = &self.source[stmt.range()].trim(); + mermaid_write_quoted_str(f, code_line)?; + write!(f, "\\n")?; + } + } + writeln!(f, "\"{close}")?; + } + writeln!(f)?; + + // Then link all the blocks. + writeln!(f, " start --> block{}", self.graph.blocks.len() - 1)?; + for (i, block) in self.graph.blocks.iter_enumerated().rev() { + let i = i.as_u32(); + match &block.next { + NextBlock::Always(target) => { + writeln!(f, " block{i} --> block{target}", target = target.as_u32())?; + } + NextBlock::If { + condition, + next, + orelse, + } => { + let condition_code = &self.source[condition.range()].trim(); + writeln!( + f, + " block{i} -- \"{condition_code}\" --> block{next}", + next = next.as_u32() + )?; + writeln!( + f, + " block{i} -- \"else\" --> block{orelse}", + orelse = orelse.as_u32() + )?; + } + NextBlock::Terminate => writeln!(f, " block{i} --> return")?, + } + } + + Ok(()) + } +} + +/// Escape double quotes (`"`) in `value` using `#quot;`. +fn mermaid_write_quoted_str(f: &mut fmt::Formatter<'_>, value: &str) -> fmt::Result { + let mut parts = value.split('"'); + if let Some(v) = parts.next() { + write!(f, "{v}")?; + } + for v in parts { + write!(f, "#quot;{v}")?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use rustpython_parser::ast::Ranged; + use rustpython_parser::{parse, Mode}; + use std::fmt::Write; + use test_case::test_case; + + use crate::rules::ruff::rules::unreachable::{ + BasicBlocks, BlockIndex, MermaidGraph, NextBlock, + }; + + #[test_case("simple.py")] + #[test_case("if.py")] + #[test_case("while.py")] + #[test_case("for.py")] + #[test_case("async-for.py")] + //#[test_case("try.py")] // TODO. + #[test_case("raise.py")] + #[test_case("assert.py")] + #[test_case("match.py")] + fn control_flow_graph(filename: &str) { + let path = PathBuf::from_iter(["resources/test/fixtures/control-flow-graph", filename]); + let source = fs::read_to_string(&path).expect("failed to read file"); + let stmts = parse(&source, Mode::Module, filename) + .unwrap_or_else(|err| panic!("failed to parse source: '{source}': {err}")) + .expect_module() + .body; + + let mut output = String::new(); + + for (i, stmts) in stmts.into_iter().enumerate() { + let Some(func) = stmts.function_def_stmt() else { + use std::io::Write; + let _ = std::io::stderr().write_all(b"unexpected statement kind, ignoring"); + continue; + }; + + let got = BasicBlocks::from(&*func.body); + // Basic sanity checks. + assert!(!got.blocks.is_empty(), "basic blocks should never be empty"); + assert_eq!( + got.blocks.first().unwrap().next, + NextBlock::Terminate, + "first block should always terminate" + ); + + // All block index should be valid. + let valid = BlockIndex::from_usize(got.blocks.len()); + for block in &got.blocks { + match block.next { + NextBlock::Always(index) => assert!(index < valid, "invalid block index"), + NextBlock::If { next, orelse, .. } => { + assert!(next < valid, "invalid next block index"); + assert!(orelse < valid, "invalid orelse block index"); + } + NextBlock::Terminate => {} + } + } + + let got_mermaid = MermaidGraph { + graph: &got, + source: &source, + }; + + writeln!( + output, + "## Function {i}\n### Source\n```python\n{}\n```\n\n### Control Flow Graph\n```mermaid\n{}```\n", + &source[func.range()], + got_mermaid + ) + .unwrap(); + } + + insta::with_settings!({ + omit_expression => true, + input_file => filename, + description => "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." + }, { + insta::assert_snapshot!(format!("{filename}.md"), output); + }); + } +} diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF014_RUF014.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF014_RUF014.py.snap new file mode 100644 index 0000000000..f2457017e3 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF014_RUF014.py.snap @@ -0,0 +1,249 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF014.py:3:5: RUF014 Unreachable code in after_return + | +1 | def after_return(): +2 | return "reachable" +3 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +4 | +5 | async def also_works_on_async_functions(): + | + +RUF014.py:7:5: RUF014 Unreachable code in also_works_on_async_functions + | +5 | async def also_works_on_async_functions(): +6 | return "reachable" +7 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +8 | +9 | def if_always_true(): + | + +RUF014.py:12:5: RUF014 Unreachable code in if_always_true + | +10 | if True: +11 | return "reachable" +12 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +13 | +14 | def if_always_false(): + | + +RUF014.py:16:9: RUF014 Unreachable code in if_always_false + | +14 | def if_always_false(): +15 | if False: +16 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +17 | return "reachable" + | + +RUF014.py:21:9: RUF014 Unreachable code in if_elif_always_false + | +19 | def if_elif_always_false(): +20 | if False: +21 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +22 | elif False: +23 | return "also unreachable" + | + +RUF014.py:23:9: RUF014 Unreachable code in if_elif_always_false + | +21 | return "unreachable" +22 | elif False: +23 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +24 | return "reachable" + | + +RUF014.py:28:9: RUF014 Unreachable code in if_elif_always_true + | +26 | def if_elif_always_true(): +27 | if False: +28 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +29 | elif True: +30 | return "reachable" + | + +RUF014.py:31:5: RUF014 Unreachable code in if_elif_always_true + | +29 | elif True: +30 | return "reachable" +31 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +32 | +33 | def ends_with_if(): + | + +RUF014.py:35:9: RUF014 Unreachable code in ends_with_if + | +33 | def ends_with_if(): +34 | if False: +35 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +36 | else: +37 | return "reachable" + | + +RUF014.py:42:5: RUF014 Unreachable code in infinite_loop + | +40 | while True: +41 | continue +42 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +43 | +44 | ''' TODO: we could determine these, but we don't yet. + | + +RUF014.py:75:5: RUF014 Unreachable code in match_wildcard + | +73 | case _: +74 | return "reachable" +75 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +76 | +77 | def match_case_and_wildcard(status): + | + +RUF014.py:83:5: RUF014 Unreachable code in match_case_and_wildcard + | +81 | case _: +82 | return "reachable" +83 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +84 | +85 | def raise_exception(): + | + +RUF014.py:87:5: RUF014 Unreachable code in raise_exception + | +85 | def raise_exception(): +86 | raise Exception +87 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +88 | +89 | def while_false(): + | + +RUF014.py:91:9: RUF014 Unreachable code in while_false + | +89 | def while_false(): +90 | while False: +91 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +92 | return "reachable" + | + +RUF014.py:96:9: RUF014 Unreachable code in while_false_else + | +94 | def while_false_else(): +95 | while False: +96 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +97 | else: +98 | return "reachable" + | + +RUF014.py:102:9: RUF014 Unreachable code in while_false_else_return + | +100 | def while_false_else_return(): +101 | while False: +102 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +103 | else: +104 | return "reachable" + | + +RUF014.py:105:5: RUF014 Unreachable code in while_false_else_return + | +103 | else: +104 | return "reachable" +105 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +106 | +107 | def while_true(): + | + +RUF014.py:110:5: RUF014 Unreachable code in while_true + | +108 | while True: +109 | return "reachable" +110 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +111 | +112 | def while_true_else(): + | + +RUF014.py:116:9: RUF014 Unreachable code in while_true_else + | +114 | return "reachable" +115 | else: +116 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +117 | +118 | def while_true_else_return(): + | + +RUF014.py:122:9: RUF014 Unreachable code in while_true_else_return + | +120 | return "reachable" +121 | else: +122 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +123 | return "also unreachable" + | + +RUF014.py:123:5: RUF014 Unreachable code in while_true_else_return + | +121 | else: +122 | return "unreachable" +123 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +124 | +125 | def while_false_var_i(): + | + +RUF014.py:128:9: RUF014 Unreachable code in while_false_var_i + | +126 | i = 0 +127 | while False: +128 | i += 1 + | ^^^^^^ RUF014 +129 | return i + | + +RUF014.py:135:5: RUF014 Unreachable code in while_true_var_i + | +133 | while True: +134 | i += 1 +135 | return i + | ^^^^^^^^ RUF014 +136 | +137 | def while_infinite(): + | + +RUF014.py:140:5: RUF014 Unreachable code in while_infinite + | +138 | while True: +139 | pass +140 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +141 | +142 | def while_if_true(): + | + +RUF014.py:146:5: RUF014 Unreachable code in while_if_true + | +144 | if True: +145 | return "reachable" +146 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +147 | +148 | # Test case found in the Bokeh repository that trigger a false positive. + | + + diff --git a/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs b/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs index db26e7588f..f256ff0f91 100644 --- a/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs +++ b/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs @@ -44,7 +44,10 @@ pub(crate) fn useless_try_except(checker: &mut Checker, handlers: &[ExceptHandle .map(|handler| { let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { name, body, .. }) = handler; - let Some(Stmt::Raise(ast::StmtRaise { exc, cause: None, .. })) = &body.first() else { + let Some(Stmt::Raise(ast::StmtRaise { + exc, cause: None, .. + })) = &body.first() + else { return None; }; if let Some(expr) = exc { diff --git a/crates/ruff/src/settings/configuration.rs b/crates/ruff/src/settings/configuration.rs index 16143a83cb..653c328011 100644 --- a/crates/ruff/src/settings/configuration.rs +++ b/crates/ruff/src/settings/configuration.rs @@ -20,7 +20,7 @@ use crate::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::options::Options; use crate::settings::types::{ @@ -93,6 +93,7 @@ pub struct Configuration { pub pydocstyle: Option, pub pyflakes: Option, pub pylint: Option, + pub pyupgrade: Option, } impl Configuration { @@ -247,6 +248,7 @@ impl Configuration { pydocstyle: options.pydocstyle, pyflakes: options.pyflakes, pylint: options.pylint, + pyupgrade: options.pyupgrade, }) } @@ -334,6 +336,7 @@ impl Configuration { pydocstyle: self.pydocstyle.combine(config.pydocstyle), pyflakes: self.pyflakes.combine(config.pyflakes), pylint: self.pylint.combine(config.pylint), + pyupgrade: self.pyupgrade.combine(config.pyupgrade), } } } diff --git a/crates/ruff/src/settings/defaults.rs b/crates/ruff/src/settings/defaults.rs index 8ae6d16e73..987148705d 100644 --- a/crates/ruff/src/settings/defaults.rs +++ b/crates/ruff/src/settings/defaults.rs @@ -1,10 +1,11 @@ -use std::collections::HashSet; - use once_cell::sync::Lazy; use path_absolutize::path_dedot; use regex::Regex; use rustc_hash::FxHashSet; +use std::collections::HashSet; +use super::types::{FilePattern, PythonVersion}; +use super::Settings; use crate::codes::{self, RuleCodePrefix}; use crate::line_width::{LineLength, TabSize}; use crate::registry::Linter; @@ -14,13 +15,10 @@ use crate::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::types::FilePatternSet; -use super::types::{FilePattern, PythonVersion}; -use super::Settings; - pub const PREFIXES: &[RuleSelector] = &[ prefix_to_selector(RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E)), RuleSelector::Linter(Linter::Pyflakes), @@ -114,6 +112,7 @@ impl Default for Settings { pydocstyle: pydocstyle::settings::Settings::default(), pyflakes: pyflakes::settings::Settings::default(), pylint: pylint::settings::Settings::default(), + pyupgrade: pyupgrade::settings::Settings::default(), } } } diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index 0f7b961734..f00a4833b2 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -20,7 +20,7 @@ use crate::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion, SerializationFormat}; @@ -130,6 +130,7 @@ pub struct Settings { pub pydocstyle: pydocstyle::settings::Settings, pub pyflakes: pyflakes::settings::Settings, pub pylint: pylint::settings::Settings, + pub pyupgrade: pyupgrade::settings::Settings, } impl Settings { @@ -284,6 +285,10 @@ impl Settings { .pylint .map(pylint::settings::Settings::from) .unwrap_or_default(), + pyupgrade: config + .pyupgrade + .map(pyupgrade::settings::Settings::from) + .unwrap_or_default(), }) } diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index ef5a6a3597..f003b6bb5a 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -12,7 +12,7 @@ use crate::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::types::{PythonVersion, SerializationFormat, Version}; @@ -551,6 +551,9 @@ pub struct Options { #[option_group] /// Options for the `pylint` plugin. pub pylint: Option, + #[option_group] + /// Options for the `pyupgrade` plugin. + pub pyupgrade: Option, // Tables are required to go last. #[option( default = "{}", diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 75fa5a9a11..212c59de10 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cli" -version = "0.0.275" +version = "0.0.276" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cli/src/commands/show_settings.rs b/crates/ruff_cli/src/commands/show_settings.rs index 8f91668be0..52f8a65dc1 100644 --- a/crates/ruff_cli/src/commands/show_settings.rs +++ b/crates/ruff_cli/src/commands/show_settings.rs @@ -23,7 +23,9 @@ pub(crate) fn show_settings( let Some(entry) = paths .iter() .flatten() - .sorted_by(|a, b| a.path().cmp(b.path())).next() else { + .sorted_by(|a, b| a.path().cmp(b.path())) + .next() + else { bail!("No files found under the given path"); }; let path = entry.path(); diff --git a/crates/ruff_dev/src/check_formatter_stability.rs b/crates/ruff_dev/src/check_formatter_stability.rs index 95a37e6838..b955a7245a 100644 --- a/crates/ruff_dev/src/check_formatter_stability.rs +++ b/crates/ruff_dev/src/check_formatter_stability.rs @@ -4,8 +4,9 @@ //! checking entire repositories. use std::fmt::{Display, Formatter}; -use std::io::stdout; +use std::fs::File; use std::io::Write; +use std::io::{stdout, BufWriter}; use std::panic::catch_unwind; use std::path::{Path, PathBuf}; use std::process::ExitCode; @@ -49,6 +50,9 @@ pub(crate) struct Args { /// Checks each project inside a directory #[arg(long)] pub(crate) multi_project: bool, + /// Write all errors to this file in addition to stdout + #[arg(long)] + pub(crate) error_file: Option, } /// Generate ourself a `try_parse_from` impl for `CheckArgs`. This is a strange way to use clap but @@ -69,6 +73,12 @@ pub(crate) fn main(args: &Args) -> anyhow::Result { #[allow(clippy::print_stdout)] { print!("{}", result.display(args.format)); + println!( + "Found {} stability errors in {} files in {:.2}s", + result.diagnostics.len(), + result.file_count, + result.duration.as_secs_f32(), + ); } result.is_success() @@ -114,6 +124,7 @@ fn check_multi_project(args: &Args) -> bool { match check_repo(&Args { files: vec![path.clone()], + error_file: args.error_file.clone(), ..*args }) { Ok(result) => sender.send(Message::Finished { result, path }), @@ -126,6 +137,9 @@ fn check_multi_project(args: &Args) -> bool { scope.spawn(|_| { let mut stdout = stdout().lock(); + let mut error_file = args.error_file.as_ref().map(|error_file| { + BufWriter::new(File::create(error_file).expect("Couldn't open error file")) + }); for message in receiver { match message { @@ -135,13 +149,19 @@ fn check_multi_project(args: &Args) -> bool { Message::Finished { path, result } => { total_errors += result.diagnostics.len(); total_files += result.file_count; + writeln!( stdout, - "Finished {}\n{}\n", + "Finished {} with {} files in {:.2}s", path.display(), - result.display(args.format) + result.file_count, + result.duration.as_secs_f32(), ) .unwrap(); + write!(stdout, "{}", result.display(args.format)).unwrap(); + if let Some(error_file) = &mut error_file { + write!(error_file, "{}", result.display(args.format)).unwrap(); + } all_success = all_success && result.is_success(); } Message::Failed { path, error } => { @@ -157,8 +177,10 @@ fn check_multi_project(args: &Args) -> bool { #[allow(clippy::print_stdout)] { - println!("{total_errors} stability errors in {total_files} files"); - println!("Finished in {}s", duration.as_secs_f32()); + println!( + "{total_errors} stability errors in {total_files} files in {}s", + duration.as_secs_f32() + ); } all_success @@ -295,23 +317,11 @@ struct DisplayCheckRepoResult<'a> { } impl Display for DisplayCheckRepoResult<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let CheckRepoResult { - duration, - file_count, - diagnostics, - } = self.result; - - for diagnostic in diagnostics { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for diagnostic in &self.result.diagnostics { write!(f, "{}", diagnostic.display(self.format))?; } - - writeln!( - f, - "Formatting {} files twice took {:.2}s", - file_count, - duration.as_secs_f32() - ) + Ok(()) } } diff --git a/crates/ruff_dev/src/generate_json_schema.rs b/crates/ruff_dev/src/generate_json_schema.rs index bde0f4abdc..d284783d9f 100644 --- a/crates/ruff_dev/src/generate_json_schema.rs +++ b/crates/ruff_dev/src/generate_json_schema.rs @@ -61,7 +61,7 @@ mod tests { use super::{main, Args}; - #[test] + #[cfg_attr(not(feature = "unreachable-code"), test)] fn test_generate_json_schema() -> Result<()> { let mode = if env::var("RUFF_UPDATE_SCHEMA").as_deref() == Ok("1") { Mode::Write diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 4e6bfe280f..4e9c5fe681 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -46,18 +46,24 @@ pub(crate) fn generate() -> String { // Generate all the top-level fields. for (name, entry) in &sorted_options { - let OptionEntry::Field(field) = entry else { continue; }; + let OptionEntry::Field(field) = entry else { + continue; + }; emit_field(&mut output, name, field, None); output.push_str("---\n\n"); } // Generate all the sub-groups. for (group_name, entry) in &sorted_options { - let OptionEntry::Group(fields) = entry else { continue; }; + let OptionEntry::Group(fields) = entry else { + continue; + }; output.push_str(&format!("### `{group_name}`\n")); output.push('\n'); for (name, entry) in fields.iter().sorted_by_key(|(name, _)| name) { - let OptionEntry::Field(field) = entry else { continue; }; + let OptionEntry::Field(field) = entry else { + continue; + }; emit_field(&mut output, name, field, Some(group_name)); output.push_str("---\n\n"); } diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 4d29bfea2b..6f1b18cfea 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -102,10 +102,10 @@ pub(crate) fn generate() -> String { )); table_out.push('\n'); table_out.push('\n'); - generate_table(&mut table_out, prefix, &linter); + generate_table(&mut table_out, prefix.clone().rules(), &linter); } } else { - generate_table(&mut table_out, &linter, &linter); + generate_table(&mut table_out, linter.rules(), &linter); } } diff --git a/crates/ruff_index/src/slice.rs b/crates/ruff_index/src/slice.rs index 77401e7133..a6d3b033df 100644 --- a/crates/ruff_index/src/slice.rs +++ b/crates/ruff_index/src/slice.rs @@ -40,6 +40,11 @@ impl IndexSlice { } } + #[inline] + pub const fn first(&self) -> Option<&T> { + self.raw.first() + } + #[inline] pub const fn len(&self) -> usize { self.raw.len() @@ -63,6 +68,13 @@ impl IndexSlice { (0..self.len()).map(|n| I::new(n)) } + #[inline] + pub fn iter_enumerated( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { + self.raw.iter().enumerate().map(|(n, t)| (I::new(n), t)) + } + #[inline] pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> { self.raw.iter_mut() diff --git a/crates/ruff_macros/src/derive_message_formats.rs b/crates/ruff_macros/src/derive_message_formats.rs index f72113c223..c155ffb00b 100644 --- a/crates/ruff_macros/src/derive_message_formats.rs +++ b/crates/ruff_macros/src/derive_message_formats.rs @@ -19,7 +19,9 @@ pub(crate) fn derive_message_formats(func: &ItemFn) -> TokenStream { } fn parse_block(block: &Block, strings: &mut TokenStream) -> Result<(), TokenStream> { - let Some(Stmt::Expr(last, _)) = block.stmts.last() else {panic!("expected last statement in block to be an expression")}; + let Some(Stmt::Expr(last, _)) = block.stmts.last() else { + panic!("expected last statement in block to be an expression") + }; parse_expr(last, strings)?; Ok(()) } @@ -28,7 +30,9 @@ fn parse_expr(expr: &Expr, strings: &mut TokenStream) -> Result<(), TokenStream> match expr { Expr::Macro(mac) if mac.mac.path.is_ident("format") => { let Some(first_token) = mac.mac.tokens.to_token_stream().into_iter().next() else { - return Err(quote_spanned!(expr.span() => compile_error!("expected format! to have an argument"))) + return Err( + quote_spanned!(expr.span() => compile_error!("expected format! to have an argument")), + ); }; strings.extend(quote! {#first_token,}); Ok(()) diff --git a/crates/ruff_macros/src/map_codes.rs b/crates/ruff_macros/src/map_codes.rs index 94fbcbdac5..eeee0d7ac9 100644 --- a/crates/ruff_macros/src/map_codes.rs +++ b/crates/ruff_macros/src/map_codes.rs @@ -31,14 +31,30 @@ struct Rule { pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { let Some(last_stmt) = func.block.stmts.last() else { - return Err(Error::new(func.block.span(), "expected body to end in an expression")); + return Err(Error::new( + func.block.span(), + "expected body to end in an expression", + )); }; - let Stmt::Expr(Expr::Call(ExprCall { args: some_args, .. }), _) = last_stmt else { - return Err(Error::new(last_stmt.span(), "expected last expression to be `Some(match (..) { .. })`")); + let Stmt::Expr( + Expr::Call(ExprCall { + args: some_args, .. + }), + _, + ) = last_stmt + else { + return Err(Error::new( + last_stmt.span(), + "expected last expression to be `Some(match (..) { .. })`", + )); }; let mut some_args = some_args.into_iter(); - let (Some(Expr::Match(ExprMatch { arms, .. })), None) = (some_args.next(), some_args.next()) else { - return Err(Error::new(last_stmt.span(), "expected last expression to be `Some(match (..) { .. })`")); + let (Some(Expr::Match(ExprMatch { arms, .. })), None) = (some_args.next(), some_args.next()) + else { + return Err(Error::new( + last_stmt.span(), + "expected last expression to be `Some(match (..) { .. })`", + )); }; // Map from: linter (e.g., `Flake8Bugbear`) to rule code (e.g.,`"002"`) to rule data (e.g., @@ -139,30 +155,13 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { } output.extend(quote! { - impl IntoIterator for &#linter { - type Item = Rule; - type IntoIter = ::std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { + impl #linter { + pub fn rules(self) -> ::std::vec::IntoIter { match self { #prefix_into_iter_match_arms } } } }); } - - output.extend(quote! { - impl IntoIterator for &RuleCodePrefix { - type Item = Rule; - type IntoIter = ::std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - #(RuleCodePrefix::#linter_idents(prefix) => prefix.into_iter(),)* - } - } - } - }); - output.extend(quote! { impl RuleCodePrefix { pub fn parse(linter: &Linter, code: &str) -> Result { @@ -172,6 +171,12 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { #(Linter::#linter_idents => RuleCodePrefix::#linter_idents(#linter_idents::from_str(code).map_err(|_| crate::registry::FromCodeError::Unknown)?),)* }) } + + pub fn rules(self) -> ::std::vec::IntoIter { + match self { + #(RuleCodePrefix::#linter_idents(prefix) => prefix.clone().rules(),)* + } + } } }); @@ -328,32 +333,39 @@ fn generate_iter_impl( linter_to_rules: &BTreeMap>, linter_idents: &[&Ident], ) -> TokenStream { - let mut linter_into_iter_match_arms = quote!(); + let mut linter_rules_match_arms = quote!(); + let mut linter_all_rules_match_arms = quote!(); for (linter, map) in linter_to_rules { - let rule_paths = map - .values() - .filter(|rule| { - // Nursery rules have to be explicitly selected, so we ignore them when looking at - // linter-level selectors (e.g., `--select SIM`). - !is_nursery(&rule.group) - }) - .map(|Rule { attrs, path, .. }| { + let rule_paths = map.values().filter(|rule| !is_nursery(&rule.group)).map( + |Rule { attrs, path, .. }| { let rule_name = path.segments.last().unwrap(); quote!(#(#attrs)* Rule::#rule_name) - }); - linter_into_iter_match_arms.extend(quote! { + }, + ); + linter_rules_match_arms.extend(quote! { + Linter::#linter => vec![#(#rule_paths,)*].into_iter(), + }); + let rule_paths = map.values().map(|Rule { attrs, path, .. }| { + let rule_name = path.segments.last().unwrap(); + quote!(#(#attrs)* Rule::#rule_name) + }); + linter_all_rules_match_arms.extend(quote! { Linter::#linter => vec![#(#rule_paths,)*].into_iter(), }); } quote! { - impl IntoIterator for &Linter { - type Item = Rule; - type IntoIter = ::std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { + impl Linter { + /// Rules not in the nursery. + pub fn rules(self: &Linter) -> ::std::vec::IntoIter { match self { - #linter_into_iter_match_arms + #linter_rules_match_arms + } + } + /// All rules, including those in the nursery. + pub fn all_rules(self: &Linter) -> ::std::vec::IntoIter { + match self { + #linter_all_rules_match_arms } } } diff --git a/crates/ruff_macros/src/newtype_index.rs b/crates/ruff_macros/src/newtype_index.rs index f6524b48a9..2c1f6e14ec 100644 --- a/crates/ruff_macros/src/newtype_index.rs +++ b/crates/ruff_macros/src/newtype_index.rs @@ -36,10 +36,11 @@ pub(super) fn generate_newtype_index(item: ItemStruct) -> syn::Result Self { - assert!(value <= Self::MAX as usize); + assert!(value <= Self::MAX_VALUE as usize); // SAFETY: // * The `value < u32::MAX` guarantees that the add doesn't overflow. @@ -49,7 +50,7 @@ pub(super) fn generate_newtype_index(item: ItemStruct) -> syn::Result Self { - assert!(value <= Self::MAX); + assert!(value <= Self::MAX_VALUE); // SAFETY: // * The `value < u32::MAX` guarantees that the add doesn't overflow. diff --git a/crates/ruff_macros/src/rule_namespace.rs b/crates/ruff_macros/src/rule_namespace.rs index f1bd7f5adf..811033f8ea 100644 --- a/crates/ruff_macros/src/rule_namespace.rs +++ b/crates/ruff_macros/src/rule_namespace.rs @@ -6,10 +6,16 @@ use syn::spanned::Spanned; use syn::{Attribute, Data, DataEnum, DeriveInput, Error, ExprLit, Lit, Meta, MetaNameValue}; pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { - let DeriveInput { ident, data: Data::Enum(DataEnum { - variants, .. - }), .. } = input else { - return Err(Error::new(input.ident.span(), "only named fields are supported")); + let DeriveInput { + ident, + data: Data::Enum(DataEnum { variants, .. }), + .. + } = input + else { + return Err(Error::new( + input.ident.span(), + "only named fields are supported", + )); }; let mut parsed = Vec::new(); @@ -53,8 +59,12 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result syn::Result syn::Result<(String, String)> { - let Meta::NameValue(MetaNameValue{value: syn::Expr::Lit(ExprLit { lit: Lit::Str(doc_lit), ..}), ..}) = &doc_attr.meta else { - return Err(Error::new(doc_attr.span(), r#"expected doc attribute to be in the form of #[doc = "..."]"#)) + let Meta::NameValue(MetaNameValue { + value: + syn::Expr::Lit(ExprLit { + lit: Lit::Str(doc_lit), + .. + }), + .. + }) = &doc_attr.meta + else { + return Err(Error::new( + doc_attr.span(), + r#"expected doc attribute to be in the form of #[doc = "..."]"#, + )); }; parse_markdown_link(doc_lit.value().trim()) .map(|(name, url)| (name.to_string(), url.to_string())) diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 1c6c65e0d5..596783db47 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -4,7 +4,7 @@ use std::path::Path; use num_traits::Zero; use ruff_text_size::{TextRange, TextSize}; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashMap; use rustpython_ast::CmpOp; use rustpython_parser::ast::{ self, Arguments, Constant, ExceptHandler, Expr, Keyword, MatchCase, Pattern, Ranged, Stmt, @@ -669,25 +669,28 @@ pub fn extract_handled_exceptions(handlers: &[ExceptHandler]) -> Vec<&Expr> { handled_exceptions } -/// Return the set of all bound argument names. -pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> { - let mut arg_names: FxHashSet<&'a str> = FxHashSet::default(); - for arg_with_default in &arguments.posonlyargs { - arg_names.insert(arg_with_default.def.arg.as_str()); - } - for arg_with_default in &arguments.args { - arg_names.insert(arg_with_default.def.arg.as_str()); +/// Returns `true` if the given name is included in the given [`Arguments`]. +pub fn includes_arg_name(name: &str, arguments: &Arguments) -> bool { + if arguments + .posonlyargs + .iter() + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + .any(|arg| arg.def.arg.as_str() == name) + { + return true; } if let Some(arg) = &arguments.vararg { - arg_names.insert(arg.arg.as_str()); - } - for arg_with_default in &arguments.kwonlyargs { - arg_names.insert(arg_with_default.def.arg.as_str()); + if arg.arg.as_str() == name { + return true; + } } if let Some(arg) = &arguments.kwarg { - arg_names.insert(arg.arg.as_str()); + if arg.arg.as_str() == name { + return true; + } } - arg_names + false } /// Given an [`Expr`] that can be callable or not (like a decorator, which could @@ -1411,11 +1414,18 @@ impl Truthiness { Constant::Ellipsis => Some(true), Constant::Tuple(elts) => Some(!elts.is_empty()), }, - Expr::JoinedStr(ast::ExprJoinedStr { values, range: _range }) => { + Expr::JoinedStr(ast::ExprJoinedStr { + values, + range: _range, + }) => { if values.is_empty() { Some(false) } else if values.iter().any(|value| { - let Expr::Constant(ast::ExprConstant { value: Constant::Str(string), .. } )= &value else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Str(string), + .. + }) = &value + else { return false; }; !string.is_empty() @@ -1425,14 +1435,30 @@ impl Truthiness { None } } - Expr::List(ast::ExprList { elts, range: _range, .. }) - | Expr::Set(ast::ExprSet { elts, range: _range }) - | Expr::Tuple(ast::ExprTuple { elts, range: _range,.. }) => Some(!elts.is_empty()), - Expr::Dict(ast::ExprDict { keys, range: _range, .. }) => Some(!keys.is_empty()), + Expr::List(ast::ExprList { + elts, + range: _range, + .. + }) + | Expr::Set(ast::ExprSet { + elts, + range: _range, + }) + | Expr::Tuple(ast::ExprTuple { + elts, + range: _range, + .. + }) => Some(!elts.is_empty()), + Expr::Dict(ast::ExprDict { + keys, + range: _range, + .. + }) => Some(!keys.is_empty()), Expr::Call(ast::ExprCall { func, args, - keywords, range: _range, + keywords, + range: _range, }) => { if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { if is_iterable_initializer(id.as_str(), |id| is_builtin(id)) { diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs index 4af761ae83..d8ddc8606c 100644 --- a/crates/ruff_python_ast/src/identifier.rs +++ b/crates/ruff_python_ast/src/identifier.rs @@ -187,8 +187,9 @@ pub fn except(handler: &ExceptHandler, locator: &Locator) -> TextRange { /// Return the [`TextRange`] of the `else` token in a `For`, `AsyncFor`, or `While` statement. pub fn else_(stmt: &Stmt, locator: &Locator) -> Option { let (Stmt::For(ast::StmtFor { body, orelse, .. }) - | Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. }) - | Stmt::While(ast::StmtWhile { body, orelse, .. })) = stmt else { + | Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. }) + | Stmt::While(ast::StmtWhile { body, orelse, .. })) = stmt + else { return None; }; diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index 0dde0b475f..b115eb1754 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -241,6 +241,29 @@ The origin of Ruff's formatter is the [Rome formatter](https://github.com/rome/t e.g. the ruff_formatter crate is forked from the [rome_formatter crate](https://github.com/rome/tools/tree/main/crates/rome_formatter). The Rome repository can be a helpful reference when implementing something in the Ruff formatter +### Checking formatter stability and panics + +There are tree common problems with the formatter: The second formatting pass looks different than +the first (formatter instability or lack of idempotency), we print invalid syntax (e.g. missing +parentheses around multiline expressions) and panics (mostly in debug assertions). We test for all +of these using the `check-formatter-stability` subcommand in `ruff_dev` + +The easiest is to check CPython: + +```shell +git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff/resources/test/cpython +cargo run --bin ruff_dev -- check-formatter-stability crates/ruff/resources/test/cpython +``` + +It is also possible large number of repositories using ruff. This dataset is large (~60GB), so we +only do this occasionally: + +```shell +curl https://raw.githubusercontent.com/akx/ruff-usage-aggregate/master/data/known-github-tomls.jsonl > github_search.jsonl +python scripts/check_ecosystem.py --checkouts target/checkouts --projects github_search.jsonl -v $(which true) $(which true) +cargo run --bin ruff_dev -- check-formatter-stability --multi-project target/checkouts +``` + ## The orphan rules and trait structure For the formatter, we would like to implement `Format` from the rust_formatter crate for all AST diff --git a/crates/ruff_python_formatter/generate.py b/crates/ruff_python_formatter/generate.py index f4e83d2edf..bcbf59871a 100644 --- a/crates/ruff_python_formatter/generate.py +++ b/crates/ruff_python_formatter/generate.py @@ -28,7 +28,10 @@ nodes_file = ( node_lines = ( nodes_file.split("pub enum AnyNode {")[1].split("}")[0].strip().splitlines() ) -nodes = [node_line.split("(")[1].split("<")[0] for node_line in node_lines] +nodes = [ + node_line.split("(")[1].split(")")[0].split("::")[-1].split("<")[0] + for node_line in node_lines +] print(nodes) # %% diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py similarity index 85% rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py index 7a32b6cd28..8c372180ce 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py @@ -81,3 +81,8 @@ f( dict() ) +# Don't add a magic trailing comma when there is only one entry +# Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 +f( + a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py index 2ec1c0e293..f0fedc6957 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py @@ -8,3 +8,16 @@ a2 = [ # a a3 = [ # b ] + +# Add magic trailing comma only if there is more than one entry, but respect it if it's +# already there +b1 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] +b2 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] +b3 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py new file mode 100644 index 0000000000..1677400431 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py @@ -0,0 +1,3 @@ +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py new file mode 100644 index 0000000000..335e91036a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py @@ -0,0 +1,16 @@ +from a import aksjdhflsakhdflkjsadlfajkslhf +from a import ( + aksjdhflsakhdflkjsadlfajkslhf, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa import * diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 97a70dbe47..4541acda74 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -182,7 +182,10 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { pub(crate) struct JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { result: FormatResult<()>, fmt: &'fmt mut PyFormatter<'ast, 'buf>, - last_end: Option, + end_of_last_entry: Option, + /// We need to track whether we have more than one entry since a sole entry doesn't get a + /// magic trailing comma even when expanded + len: usize, } impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { @@ -190,7 +193,8 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { Self { fmt: f, result: Ok(()), - last_end: None, + end_of_last_entry: None, + len: 0, } } @@ -203,11 +207,12 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { T: Ranged, { self.result = self.result.and_then(|_| { - if self.last_end.is_some() { + if self.end_of_last_entry.is_some() { write!(self.fmt, [text(","), soft_line_break_or_space()])?; } - self.last_end = Some(node.end()); + self.end_of_last_entry = Some(node.end()); + self.len += 1; content.fmt(self.fmt) }); @@ -243,18 +248,23 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { pub(crate) fn finish(&mut self) -> FormatResult<()> { self.result.and_then(|_| { - if let Some(last_end) = self.last_end.take() { - if_group_breaks(&text(",")).fmt(self.fmt)?; - - if self.fmt.options().magic_trailing_comma().is_respect() + if let Some(last_end) = self.end_of_last_entry.take() { + let magic_trailing_comma = self.fmt.options().magic_trailing_comma().is_respect() && matches!( first_non_trivia_token(last_end, self.fmt.context().contents()), Some(Token { kind: TokenKind::Comma, .. }) - ) - { + ); + + // If there is a single entry, only keep the magic trailing comma, don't add it if + // it wasn't there. If there is more than one entry, always add it. + if magic_trailing_comma || self.len > 1 { + if_group_breaks(&text(",")).fmt(self.fmt)?; + } + + if magic_trailing_comma { expand_parent().fmt(self.fmt)?; } } diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 9e31caddbb..2692b0e27d 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -67,14 +67,12 @@ fn handle_match_comment<'a>( // Get the enclosing match case let Some(match_case) = comment.enclosing_node().match_case() else { - return CommentPlacement::Default(comment) + return CommentPlacement::Default(comment); }; // And its parent match statement. - let Some(match_stmt) = comment - .enclosing_parent() - .and_then(AnyNodeRef::stmt_match) else { - return CommentPlacement::Default(comment) + let Some(match_stmt) = comment.enclosing_parent().and_then(AnyNodeRef::stmt_match) else { + return CommentPlacement::Default(comment); }; // Get the next sibling (sibling traversal would be really nice) @@ -163,7 +161,9 @@ fn handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comme return CommentPlacement::Default(comment); } - let (Some(AnyNodeRef::ExceptHandlerExceptHandler(preceding_except_handler)), Some(following)) = (comment.preceding_node(), comment.following_node()) else { + let (Some(AnyNodeRef::ExceptHandlerExceptHandler(preceding_except_handler)), Some(following)) = + (comment.preceding_node(), comment.following_node()) + else { return CommentPlacement::Default(comment); }; @@ -175,10 +175,10 @@ fn handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comme .unwrap_or_default(); let Some(except_indentation) = - whitespace::indentation(locator, preceding_except_handler).map(str::len) else - { - return CommentPlacement::Default(comment); - }; + whitespace::indentation(locator, preceding_except_handler).map(str::len) + else { + return CommentPlacement::Default(comment); + }; if comment_indentation > except_indentation { // Delegate to `handle_trailing_body_comment` @@ -447,7 +447,9 @@ fn handle_trailing_body_comment<'a>( return CommentPlacement::Default(comment); }; - let Some(comment_indentation) = whitespace::indentation_at_offset(locator, comment.slice().range().start()) else { + let Some(comment_indentation) = + whitespace::indentation_at_offset(locator, comment.slice().range().start()) + else { // The comment can't be a comment for the previous block if it isn't indented.. return CommentPlacement::Default(comment); }; @@ -465,7 +467,9 @@ fn handle_trailing_body_comment<'a>( // # Trailing if comment // ``` // Here we keep the comment a trailing comment of the `if` - let Some(preceding_node_indentation) = whitespace::indentation_at_offset(locator, preceding_node.start()) else { + let Some(preceding_node_indentation) = + whitespace::indentation_at_offset(locator, preceding_node.start()) + else { return CommentPlacement::Default(comment); }; if comment_indentation_len == preceding_node_indentation.len() { @@ -593,7 +597,8 @@ fn handle_trailing_end_of_line_condition_comment<'a>( } // Must be between the condition expression and the first body element - let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) else { + let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) + else { return CommentPlacement::Default(comment); }; @@ -881,8 +886,9 @@ fn handle_module_level_own_line_comment_before_class_or_function_comment<'a>( } // ... for comments with a preceding and following node, - let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) else { - return CommentPlacement::Default(comment) + let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) + else { + return CommentPlacement::Default(comment); }; // ... where the following is a function or class statement. diff --git a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs index e9323cf712..8fb9b42eb2 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs @@ -49,7 +49,7 @@ impl<'ast> FormatBinaryLike<'ast> for ExprBoolOp { let comments = f.context().comments().clone(); let Some(first) = values.next() else { - return Ok(()) + return Ok(()); }; write!(f, [group(&first.format())])?; diff --git a/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs b/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs index 54762794e3..8cf7e8d38c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs @@ -11,7 +11,12 @@ pub struct FormatExprGeneratorExp; impl FormatNodeRule for FormatExprGeneratorExp { fn fmt_fields(&self, _item: &ExprGeneratorExp, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented_custom_text("(i for i in [])")]) + write!( + f, + [not_yet_implemented_custom_text( + "(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])" + )] + ) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs index 3ab6a61f06..5bc6a3017a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs @@ -11,7 +11,12 @@ pub struct FormatExprListComp; impl FormatNodeRule for FormatExprListComp { fn fmt_fields(&self, _item: &ExprListComp, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented_custom_text("[i for i in []]")]) + write!( + f, + [not_yet_implemented_custom_text( + "[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []]" + )] + ) } } diff --git a/crates/ruff_python_formatter/src/other/alias.rs b/crates/ruff_python_formatter/src/other/alias.rs index 8a1501e09c..f59dd012bd 100644 --- a/crates/ruff_python_formatter/src/other/alias.rs +++ b/crates/ruff_python_formatter/src/other/alias.rs @@ -1,5 +1,6 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; use rustpython_parser::ast::Alias; #[derive(Default)] @@ -7,6 +8,15 @@ pub struct FormatAlias; impl FormatNodeRule for FormatAlias { fn fmt_fields(&self, item: &Alias, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let Alias { + range: _, + name, + asname, + } = item; + name.format().fmt(f)?; + if let Some(asname) = asname { + write!(f, [space(), text("as"), space(), asname.format()])?; + } + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_import.rs b/crates/ruff_python_formatter/src/statement/stmt_import.rs index 2585dfade7..10aec39721 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import.rs @@ -1,4 +1,5 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::{FormatNodeRule, FormattedIterExt, PyFormatter}; +use ruff_formatter::prelude::{format_args, format_with, space, text}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::StmtImport; @@ -7,6 +8,12 @@ pub struct FormatStmtImport; impl FormatNodeRule for FormatStmtImport { fn fmt_fields(&self, item: &StmtImport, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtImport { names, range: _ } = item; + let names = format_with(|f| { + f.join_with(&format_args![text(","), space()]) + .entries(names.iter().formatted()) + .finish() + }); + write!(f, [text("import"), space(), names]) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs index bdae4d56ba..ef8cc13584 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs @@ -1,5 +1,7 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::builders::{optional_parentheses, PyFormatterExtensions}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{dynamic_text, format_with, space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; use rustpython_parser::ast::StmtImportFrom; #[derive(Default)] @@ -7,6 +9,40 @@ pub struct FormatStmtImportFrom; impl FormatNodeRule for FormatStmtImportFrom { fn fmt_fields(&self, item: &StmtImportFrom, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtImportFrom { + module, + names, + range: _, + level, + } = item; + + let level_str = level + .map(|level| ".".repeat(level.to_usize())) + .unwrap_or(String::default()); + + write!( + f, + [ + text("from"), + space(), + dynamic_text(&level_str, None), + module.as_ref().map(AsFormat::format), + space(), + text("import"), + space(), + ] + )?; + if let [name] = names.as_slice() { + // star can't be surrounded by parentheses + if name.name.as_str() == "*" { + return text("*").fmt(f); + } + } + let names = format_with(|f| { + f.join_comma_separated() + .entries(names.iter().map(|name| (name, name.format()))) + .finish() + }); + optional_parentheses(&names).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index d16576a6db..be25b63399 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -52,7 +52,7 @@ impl FormatRule> for FormatSuite { let mut iter = statements.iter(); let Some(first) = iter.next() else { - return Ok(()) + return Ok(()); }; // First entry has never any separator, doesn't matter which one we take; diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index 942e75d341..86516170a1 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -245,7 +245,7 @@ impl<'a> SimpleTokenizer<'a> { return Token { kind: TokenKind::EndOfFile, range: TextRange::empty(self.offset), - } + }; }; if self.bogus { @@ -310,7 +310,7 @@ impl<'a> SimpleTokenizer<'a> { return Token { kind: TokenKind::EndOfFile, range: TextRange::empty(self.back_offset), - } + }; }; if self.bogus { diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap index 5ab4cf643f..cc7b50679b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap @@ -79,7 +79,7 @@ def something(): ```diff --- Black +++ Ruff -@@ -1,90 +1,48 @@ +@@ -1,90 +1,50 @@ long_kwargs_single_line = my_function( foo="test, this is a sample value", - bar=( @@ -157,21 +157,21 @@ def something(): - ) - for some_boolean_variable in some_iterable -) -+generator_expression = (i for i in []) ++generator_expression = (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def limit_offset_sql(self, low_mark, high_mark): """Return LIMIT/OFFSET SQL clause.""" limit, offset = self._get_limit_offset_params(low_mark, high_mark) -- return " ".join( + return " ".join( - sql - for sql in ( - "LIMIT %d" % limit if limit else None, - ("OFFSET %d" % offset) if offset else None, - ) - if sql -- ) -+ return " ".join((i for i in [])) ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + ) def something(): @@ -221,13 +221,15 @@ def weird_default_argument( nested = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -generator_expression = (i for i in []) +generator_expression = (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def limit_offset_sql(self, low_mark, high_mark): """Return LIMIT/OFFSET SQL clause.""" limit, offset = self._get_limit_offset_params(low_mark, high_mark) - return " ".join((i for i in [])) + return " ".join( + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + ) def something(): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap index 52fbac942f..f8f00344e5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap @@ -43,21 +43,20 @@ def eggs() -> Union[str, int]: ... --- Black +++ Ruff @@ -1,32 +1,58 @@ --from typing import Union -+NOT_YET_IMPLEMENTED_StmtImportFrom -+ + from typing import Union ++ @bird -def zoo(): ... +def zoo(): + ... -+ -+ -+class A: -+ ... -class A: ... ++class A: ++ ... ++ ++ @bar class B: - def BMethod(self) -> None: ... @@ -94,14 +93,14 @@ def eggs() -> Union[str, int]: ... + +class F(A, C): + ... ++ ++ ++def spam() -> None: ++ ... -class F(A, C): ... -def spam() -> None: ... -+def spam() -> None: -+ ... -+ -+ @overload -def spam(arg: str) -> str: ... +def spam(arg: str) -> str: @@ -120,7 +119,7 @@ def eggs() -> Union[str, int]: ... ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from typing import Union @bird diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap index e969e1e7a7..a3f36ae2e6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap @@ -312,15 +312,6 @@ long_unmergable_string_with_pragma = ( y = "Short string" -@@ -12,7 +12,7 @@ - ) - - print( -- "This is a really long string inside of a print statement with no extra arguments attached at the end of it." -+ "This is a really long string inside of a print statement with no extra arguments attached at the end of it.", - ) - - D1 = { @@ -70,8 +70,8 @@ bad_split3 = ( "What if we have inline comments on " # First Comment @@ -367,7 +358,7 @@ long_unmergable_string_with_pragma = ( comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. -@@ -165,30 +163,18 @@ +@@ -165,25 +163,13 @@ triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" @@ -397,12 +388,6 @@ long_unmergable_string_with_pragma = ( some_function_call( "With a reallly generic name and with a really really long string that is, at some point down the line, " - + added -- + " to a variable and then added to another string." -+ + " to a variable and then added to another string.", - ) - - some_function_call( @@ -212,29 +198,25 @@ ) @@ -412,7 +397,7 @@ long_unmergable_string_with_pragma = ( - " which should NOT be there." - ), + "This is a really long string argument to a function that has a trailing comma" -+ " which should NOT be there.", ++ " which should NOT be there." ) func_with_bad_comma( @@ -421,7 +406,7 @@ long_unmergable_string_with_pragma = ( - " which should NOT be there." - ), # comment after comma + "This is a really long string argument to a function that has a trailing comma" -+ " which should NOT be there.", # comment after comma ++ " which should NOT be there." # comment after comma ) func_with_bad_parens_that_wont_fit_in_one_line( @@ -498,7 +483,7 @@ print( ) print( - "This is a really long string inside of a print statement with no extra arguments attached at the end of it.", + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." ) D1 = { @@ -660,7 +645,7 @@ NOT_YET_IMPLEMENTED_StmtAssert some_function_call( "With a reallly generic name and with a really really long string that is, at some point down the line, " + added - + " to a variable and then added to another string.", + + " to a variable and then added to another string." ) some_function_call( @@ -685,12 +670,12 @@ func_with_bad_comma( func_with_bad_comma( "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there.", + " which should NOT be there." ) func_with_bad_comma( "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there.", # comment after comma + " which should NOT be there." # comment after comma ) func_with_bad_parens_that_wont_fit_in_one_line( diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap index 9afb2c247d..8752abc340 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap @@ -132,8 +132,7 @@ match bar1: --- Black +++ Ruff @@ -1,119 +1,43 @@ --import match -+NOT_YET_IMPLEMENTED_StmtImport + import match -match something: - case [a as b]: @@ -208,10 +207,11 @@ match bar1: - ), - ): - pass -- ++NOT_YET_IMPLEMENTED_StmtMatch + - case [a as match]: - pass -- + - case case: - pass +NOT_YET_IMPLEMENTED_StmtMatch @@ -220,8 +220,9 @@ match bar1: -match match: - case case: - pass -- -- ++NOT_YET_IMPLEMENTED_StmtMatch + + -match a, *b(), c: - case d, *f, g: - pass @@ -236,30 +237,28 @@ match bar1: - pass - case {"maybe": something(complicated as this) as that}: - pass +- +NOT_YET_IMPLEMENTED_StmtMatch - -match something: - case 1 as a: - pass -+NOT_YET_IMPLEMENTED_StmtMatch - case 2 as b, 3 as c: - pass ++NOT_YET_IMPLEMENTED_StmtMatch - case 4 as d, (5 as e), (6 | 7 as g), *h: - pass -+NOT_YET_IMPLEMENTED_StmtMatch - +- -match bar1: - case Foo(aa=Callable() as aa, bb=int()): - print(bar1.aa, bar1.bb) - case _: - print("no match", "\n") -+NOT_YET_IMPLEMENTED_StmtMatch - - +- +- -match bar1: - case Foo( - normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u @@ -271,7 +270,7 @@ match bar1: ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import match NOT_YET_IMPLEMENTED_StmtMatch diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap index e1aef4ad1b..0f93e60a69 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap @@ -134,7 +134,7 @@ with match() as match: def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: -@@ -23,13 +23,9 @@ +@@ -23,13 +23,11 @@ pygram.python_grammar, ] @@ -146,11 +146,13 @@ with match() as match: + NOT_YET_IMPLEMENTED_StmtMatch - if all(version.is_python2() for version in target_versions): -+ if all((i for i in [])): ++ if all( ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++ ): # Python 2-only code, so try Python 2 grammars. return [ # Python 2.7 with future print_function import -@@ -41,13 +37,11 @@ +@@ -41,13 +39,11 @@ re.match() match = a with match() as match: @@ -166,7 +168,7 @@ with match() as match: self.assertIs(x, False) self.assertEqual(y, 0) self.assertIs(z, x) -@@ -72,16 +66,12 @@ +@@ -72,16 +68,12 @@ def test_patma_155(self): x = 0 y = None @@ -185,7 +187,7 @@ with match() as match: # At least one of the above branches must have been taken, because every Python # version has exactly one of the two 'ASYNC_*' flags -@@ -91,7 +81,7 @@ +@@ -91,7 +83,7 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: """Given a string with source, return the lib2to3 Node.""" if not src_txt.endswith("\n"): @@ -194,7 +196,7 @@ with match() as match: grammars = get_grammars(set(target_versions)) -@@ -99,9 +89,9 @@ +@@ -99,9 +91,9 @@ re.match() match = a with match() as match: @@ -238,7 +240,9 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: NOT_YET_IMPLEMENTED_StmtMatch - if all((i for i in [])): + if all( + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + ): # Python 2-only code, so try Python 2 grammars. return [ # Python 2.7 with future print_function import diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap index 6a8135be42..56fe93fb72 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap @@ -84,7 +84,7 @@ match match( -match(arg) # comment +match( -+ arg, # comment ++ arg # comment +) match() @@ -93,7 +93,7 @@ match match( -case(arg) # comment +case( -+ arg, # comment ++ arg # comment +) case() @@ -103,7 +103,7 @@ match match( -re.match(something) # fast +re.match( -+ something, # fast ++ something # fast +) re.match() -match match(): @@ -120,7 +120,7 @@ match match( NOT_YET_IMPLEMENTED_StmtMatch match( - arg, # comment + arg # comment ) match() @@ -128,7 +128,7 @@ match() match() case( - arg, # comment + arg # comment ) case() @@ -137,7 +137,7 @@ case() re.match( - something, # fast + something # fast ) re.match() NOT_YET_IMPLEMENTED_StmtMatch diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap index 64f4277f8b..974a2dc217 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap @@ -27,7 +27,7 @@ f(x, (a := b + c for c in range(10)), y=z, **q) ```diff --- Black +++ Ruff -@@ -1,15 +1,15 @@ +@@ -1,15 +1,20 @@ # Unparenthesized walruses are now allowed in indices since Python 3.10. -x[a:=0] -x[a:=0, b:=1] @@ -38,7 +38,7 @@ f(x, (a := b + c for c in range(10)), y=z, **q) # Walruses are allowed inside generator expressions on function calls since 3.10. -if any(match := pattern_error.match(s) for s in buffer): -+if any((i for i in [])): ++if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): if match.group(2) == data_not_available: # Error OK to ignore. pass @@ -47,10 +47,15 @@ f(x, (a := b + c for c in range(10)), y=z, **q) -f((a := b + c for c in range(10)), x) -f(y=(a := b + c for c in range(10))) -f(x, (a := b + c for c in range(10)), y=z, **q) -+f((i for i in [])) -+f((i for i in []), x) -+f(y=(i for i in [])) -+f(x, (i for i in []), y=z, **q) ++f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) ++f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), x) ++f(y=(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) ++f( ++ x, ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), ++ y=z, ++ **q, ++) ``` ## Ruff Output @@ -62,15 +67,20 @@ x[NOT_YET_IMPLEMENTED_ExprNamedExpr, NOT_YET_IMPLEMENTED_ExprNamedExpr] x[5, NOT_YET_IMPLEMENTED_ExprNamedExpr] # Walruses are allowed inside generator expressions on function calls since 3.10. -if any((i for i in [])): +if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): if match.group(2) == data_not_available: # Error OK to ignore. pass -f((i for i in [])) -f((i for i in []), x) -f(y=(i for i in [])) -f(x, (i for i in []), y=z, **q) +f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) +f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), x) +f(y=(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) +f( + x, + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), + y=z, + **q, +) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap index 47302f6806..785d395e09 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap @@ -47,7 +47,7 @@ def make_arange(n): def f(): - return (i * 2 async for i in arange(42)) -+ return (i for i in []) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def g(): @@ -55,7 +55,7 @@ def make_arange(n): - something_long * something_long - async for something_long in async_generator(with_an_argument) - ) -+ return (i for i in []) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) async def func(): @@ -66,17 +66,17 @@ def make_arange(n): - self.async_inc, arange(8), batch_size=3 - ) - ] -+ out_batched = [i for i in []] ++ out_batched = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] def awaited_generator_value(n): - return (await awaitable for awaitable in awaitable_list) -+ return (i for i in []) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def make_arange(n): - return (i * 2 for i in range(n) if await wrap(i)) -+ return (i for i in []) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ``` ## Ruff Output @@ -86,24 +86,24 @@ def make_arange(n): def f(): - return (i for i in []) + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def g(): - return (i for i in []) + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) async def func(): if test: - out_batched = [i for i in []] + out_batched = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] def awaited_generator_value(n): - return (i for i in []) + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def make_arange(n): - return (i for i in []) + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap index 5e1fa92d6b..a5be560338 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap @@ -76,7 +76,7 @@ while x := f(x): -y0 = (y1 := f(x)) -foo(x=(y := f(x))) +[NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3] -+filtered_data = [i for i in []] ++filtered_data = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +y0 = NOT_YET_IMPLEMENTED_ExprNamedExpr +foo(x=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) @@ -117,7 +117,7 @@ while x := f(x): +len(NOT_YET_IMPLEMENTED_ExprNamedExpr) +foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, cat="vector") +foo(cat=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) -+if any((i for i in [])): ++if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): print(longline) -if env_base := os.environ.get("PYTHONUSERBASE", None): +if NOT_YET_IMPLEMENTED_ExprNamedExpr: @@ -150,7 +150,7 @@ if (NOT_YET_IMPLEMENTED_ExprNamedExpr) is None: if NOT_YET_IMPLEMENTED_ExprNamedExpr: pass [NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3] -filtered_data = [i for i in []] +filtered_data = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] (NOT_YET_IMPLEMENTED_ExprNamedExpr) y0 = NOT_YET_IMPLEMENTED_ExprNamedExpr foo(x=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) @@ -176,7 +176,7 @@ x = NOT_YET_IMPLEMENTED_ExprNamedExpr len(NOT_YET_IMPLEMENTED_ExprNamedExpr) foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, cat="vector") foo(cat=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) -if any((i for i in [])): +if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): print(longline) if NOT_YET_IMPLEMENTED_ExprNamedExpr: return env_base diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap index 51c59c4e9d..cf66667382 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap @@ -83,31 +83,9 @@ if True: ```diff --- Black +++ Ruff -@@ -1,40 +1,22 @@ --import core, time, a -+NOT_YET_IMPLEMENTED_StmtImport - --from . import A, B, C -+NOT_YET_IMPLEMENTED_StmtImportFrom - - # keeps existing trailing comma --from foo import ( -- bar, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom - - # also keeps existing structure --from foo import ( -- baz, -- qux, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom - - # `as` works as well --from foo import ( -- xyzzy as magic, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom +@@ -18,23 +18,12 @@ + xyzzy as magic, + ) -a = { - 1, @@ -132,7 +110,7 @@ if True: nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} nested_long_lines = [ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -@@ -52,10 +34,7 @@ +@@ -52,10 +41,7 @@ y = { "oneple": (1,), } @@ -149,18 +127,25 @@ if True: ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import core, time, a -NOT_YET_IMPLEMENTED_StmtImportFrom +from . import A, B, C # keeps existing trailing comma -NOT_YET_IMPLEMENTED_StmtImportFrom +from foo import ( + bar, +) # also keeps existing structure -NOT_YET_IMPLEMENTED_StmtImportFrom +from foo import ( + baz, + qux, +) # `as` works as well -NOT_YET_IMPLEMENTED_StmtImportFrom +from foo import ( + xyzzy as magic, +) a = {1, 2, 3} b = {1, 2, 3} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap deleted file mode 100644 index d78df8d356..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap +++ /dev/null @@ -1,347 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py ---- -## Input - -```py -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines. -""" - -import os.path -import sys - -import a -from b.c import X # some noqa comment - -try: - import fast -except ImportError: - import slow as fast - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - import inner_imports - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -9,16 +9,16 @@ - Possibly also many, many lines. - """ - --import os.path --import sys -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport - --import a --from b.c import X # some noqa comment -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment - - try: -- import fast -+ NOT_YET_IMPLEMENTED_StmtImport - except ImportError: -- import slow as fast -+ NOT_YET_IMPLEMENTED_StmtImport - - - # Some comment before a function. -@@ -35,7 +35,7 @@ - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. -- import inner_imports -+ NOT_YET_IMPLEMENTED_StmtImport - - if inner_imports.are_evil(): - # Explains why we have this if. -``` - -## Ruff Output - -```py -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines. -""" - -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport - -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment - -try: - NOT_YET_IMPLEMENTED_StmtImport -except ImportError: - NOT_YET_IMPLEMENTED_StmtImport - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - NOT_YET_IMPLEMENTED_StmtImport - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - -## Black Output - -```py -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines. -""" - -import os.path -import sys - -import a -from b.c import X # some noqa comment - -try: - import fast -except ImportError: - import slow as fast - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - import inner_imports - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap index 0b4edc1d0f..e7ae03c930 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap @@ -177,19 +177,18 @@ instruction()#comment with bad spacing ```diff --- Black +++ Ruff -@@ -1,9 +1,5 @@ --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( +@@ -1,8 +1,8 @@ + from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY --) --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( ++ MyLovelyCompanyTeamProjectComponent # NOT DRY + ) + from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY --) -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom ++ MyLovelyCompanyTeamProjectComponent as component # DRY + ) # Please keep __all__ alphabetized within each category. - -@@ -45,7 +41,7 @@ +@@ -45,7 +45,7 @@ # user-defined types and objects Cheese, Cheese("Wensleydale"), @@ -198,7 +197,7 @@ instruction()#comment with bad spacing ] if "PYTHON" in os.environ: -@@ -60,8 +56,12 @@ +@@ -60,8 +60,12 @@ # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: @@ -212,7 +211,7 @@ instruction()#comment with bad spacing children[0], body, children[-1], # type: ignore -@@ -72,7 +72,11 @@ +@@ -72,7 +76,11 @@ body, parameters.children[-1], # )2 ] @@ -225,7 +224,7 @@ instruction()#comment with bad spacing if ( self._proc is not None # has the child process finished? -@@ -114,25 +118,9 @@ +@@ -114,25 +122,9 @@ # yup arg3=True, ) @@ -248,13 +247,13 @@ instruction()#comment with bad spacing - # right - if element is not None - ] -+ lcomp = [i for i in []] -+ lcomp2 = [i for i in []] -+ lcomp3 = [i for i in []] ++ lcomp = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++ lcomp2 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++ lcomp3 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] while True: if False: continue -@@ -143,7 +131,10 @@ +@@ -143,7 +135,10 @@ # let's return return Node( syms.simple_stmt, @@ -266,7 +265,7 @@ instruction()#comment with bad spacing ) -@@ -158,7 +149,11 @@ +@@ -158,7 +153,11 @@ class Test: def _init_host(self, parsed) -> None: @@ -284,8 +283,12 @@ instruction()#comment with bad spacing ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component # DRY +) # Please keep __all__ alphabetized within each category. @@ -404,9 +407,9 @@ short # yup arg3=True, ) - lcomp = [i for i in []] - lcomp2 = [i for i in []] - lcomp3 = [i for i in []] + lcomp = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] + lcomp2 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] + lcomp3 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] while True: if False: continue diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap index a656b77f09..2812b9d37a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap @@ -72,19 +72,10 @@ def func(): - # right - if element is not None - ] -+ lcomp3 = [i for i in []] ++ lcomp3 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] # Capture each of the exceptions in the MultiError along with each of their causes and contexts if isinstance(exc_value, MultiError): embedded = [] -@@ -29,7 +22,7 @@ - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), -- ) -+ ), - # This should be left alone (after) - ) - ``` ## Ruff Output @@ -98,7 +89,7 @@ def func(): x = """ a really long string """ - lcomp3 = [i for i in []] + lcomp3 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] # Capture each of the exceptions in the MultiError along with each of their causes and contexts if isinstance(exc_value, MultiError): embedded = [] @@ -114,7 +105,7 @@ def func(): # copy the set of _seen exceptions so that duplicates # shared between sub-exceptions are not omitted _seen=set(_seen), - ), + ) # This should be left alone (after) ) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap index 2358992e6c..7a1948330f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap @@ -106,19 +106,7 @@ def foo3(list_a, list_b): ```diff --- Black +++ Ruff -@@ -1,9 +1,5 @@ --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( -- MyLovelyCompanyTeamProjectComponent, # NOT DRY --) --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( -- MyLovelyCompanyTeamProjectComponent as component, # DRY --) -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom - - - class C: -@@ -58,37 +54,28 @@ +@@ -58,37 +58,28 @@ def foo(list_a, list_b): results = ( @@ -172,8 +160,12 @@ def foo3(list_a, list_b): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent, # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component, # DRY +) class C: diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap deleted file mode 100644 index 28f4426951..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap +++ /dev/null @@ -1,255 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments5.py ---- -## Input - -```py -while True: - if something.changed: - do.stuff() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - -# This one is properly standalone now. - -for i in range(100): - # first we do this - if i % 33 == 0: - break - - # then we do this - print(i) - # and finally we loop around - -with open(some_temp_file) as f: - data = f.read() - -try: - with open(some_other_file) as w: - w.write(data) - -except OSError: - print("problems") - -import sys - - -# leading function comment -def wat(): - ... - # trailing function comment - - -# SECTION COMMENT - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading 3 -@deco3 -def decorated1(): - ... - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading function comment -def decorated1(): - ... - - -# Note: this is fixed in -# Preview.empty_lines_before_class_or_def_with_leading_comments. -# In the current style, the user will have to split those lines by hand. -some_instruction - - -# This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... - - -if __name__ == "__main__": - main() -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -27,7 +27,7 @@ - except OSError: - print("problems") - --import sys -+NOT_YET_IMPLEMENTED_StmtImport - - - # leading function comment -``` - -## Ruff Output - -```py -while True: - if something.changed: - do.stuff() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - -# This one is properly standalone now. - -for i in range(100): - # first we do this - if i % 33 == 0: - break - - # then we do this - print(i) - # and finally we loop around - -with open(some_temp_file) as f: - data = f.read() - -try: - with open(some_other_file) as w: - w.write(data) - -except OSError: - print("problems") - -NOT_YET_IMPLEMENTED_StmtImport - - -# leading function comment -def wat(): - ... - # trailing function comment - - -# SECTION COMMENT - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading 3 -@deco3 -def decorated1(): - ... - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading function comment -def decorated1(): - ... - - -# Note: this is fixed in -# Preview.empty_lines_before_class_or_def_with_leading_comments. -# In the current style, the user will have to split those lines by hand. -some_instruction - - -# This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... - - -if __name__ == "__main__": - main() -``` - -## Black Output - -```py -while True: - if something.changed: - do.stuff() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - -# This one is properly standalone now. - -for i in range(100): - # first we do this - if i % 33 == 0: - break - - # then we do this - print(i) - # and finally we loop around - -with open(some_temp_file) as f: - data = f.read() - -try: - with open(some_other_file) as w: - w.write(data) - -except OSError: - print("problems") - -import sys - - -# leading function comment -def wat(): - ... - # trailing function comment - - -# SECTION COMMENT - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading 3 -@deco3 -def decorated1(): - ... - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading function comment -def decorated1(): - ... - - -# Note: this is fixed in -# Preview.empty_lines_before_class_or_def_with_leading_comments. -# In the current style, the user will have to split those lines by hand. -some_instruction - - -# This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... - - -if __name__ == "__main__": - main() -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap index 511d9fe1b1..a27f99c5bf 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap @@ -130,12 +130,6 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite ```diff --- Black +++ Ruff -@@ -1,4 +1,4 @@ --from typing import Any, Tuple -+NOT_YET_IMPLEMENTED_StmtImportFrom - - - def f( @@ -49,9 +49,7 @@ element = 0 # type: int another_element = 1 # type: float @@ -192,7 +186,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from typing import Any, Tuple def f( diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap index 12fd5c1269..e8c73055d0 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap @@ -31,18 +31,7 @@ def function(a:int=42): ```diff --- Black +++ Ruff -@@ -1,9 +1,4 @@ --from .config import ( -- ConfigTypeAttributes, -- Int, -- Path, # String, -- # DEFAULT_TYPE_ATTRIBUTES, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom - - result = 1 # A simple comment - result = (1,) # Another one -@@ -14,9 +9,9 @@ +@@ -14,9 +14,9 @@ def function(a: int = 42): @@ -60,7 +49,12 @@ def function(a:int=42): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from .config import ( + ConfigTypeAttributes, + Int, + Path, # String, + # DEFAULT_TYPE_ATTRIBUTES, +) result = 1 # A simple comment result = (1,) # Another one diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap index deda69dc2e..399e4ab915 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap @@ -203,15 +203,6 @@ class C: print(i) xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( push_manager=context.request.resource_manager, -@@ -37,7 +37,7 @@ - batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, - ).push( - # Only send the first n items. -- items=items[:num_items] -+ items=items[:num_items], - ) - return ( - 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' @@ -47,113 +47,46 @@ def omitting_trailers(self) -> None: get_collection( @@ -418,7 +409,7 @@ class C: batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, ).push( # Only send the first n items. - items=items[:num_items], + items=items[:num_items] ) return ( 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap index 993bab559a..69415159ce 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap @@ -203,15 +203,6 @@ class C: print(i) xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( push_manager=context.request.resource_manager, -@@ -37,7 +37,7 @@ - batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, - ).push( - # Only send the first n items. -- items=items[:num_items] -+ items=items[:num_items], - ) - return ( - 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' @@ -47,113 +47,46 @@ def omitting_trailers(self) -> None: get_collection( @@ -418,7 +409,7 @@ class C: batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, ).push( # Only send the first n items. - items=items[:num_items], + items=items[:num_items] ) return ( 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap index 86ac68c37e..729478a04b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap @@ -384,10 +384,10 @@ last_call() +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} -+[i for i in []] -+[i for i in []] -+[i for i in []] -+[i for i in []] ++[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} @@ -486,10 +486,10 @@ last_call() -((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) -(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) -(*starred,) -+(i for i in []) -+(i for i in []) -+(i for i in []) -+(i for i in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(*NOT_YET_IMPLEMENTED_ExprStarred,) { "id": "1", @@ -507,7 +507,7 @@ last_call() ) what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( vars_to_remove --) + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -515,7 +515,7 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() - ) +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -537,7 +537,7 @@ last_call() Ø = set() authors.łukasz.say_thanks() mapping = { -@@ -237,29 +231,27 @@ +@@ -237,29 +231,29 @@ def gen(): @@ -574,11 +574,13 @@ last_call() for y in (): ... -for z in (i for i in (1, 2, 3)): -+for z in (i for i in []): ++for ( ++ z ++) in (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): ... for i in call(): ... -@@ -328,13 +320,18 @@ +@@ -328,13 +322,18 @@ ): return True if ( @@ -600,7 +602,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -342,7 +339,8 @@ +@@ -342,7 +341,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -714,10 +716,10 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} -[i for i in []] -[i for i in []] -[i for i in []] -[i for i in []] +[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} @@ -802,10 +804,10 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false (SomeName) SomeName (Good, Bad, Ugly) -(i for i in []) -(i for i in []) -(i for i in []) -(i for i in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) (*NOT_YET_IMPLEMENTED_ExprStarred,) { "id": "1", @@ -868,7 +870,9 @@ for (x,) in (1,), (2,), (3,): ... for y in (): ... -for z in (i for i in []): +for ( + z +) in (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): ... for i in call(): ... diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap index 4d40d39ee7..013a97677b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap @@ -198,22 +198,13 @@ d={'a':1, ```diff --- Black +++ Ruff -@@ -1,15 +1,14 @@ - #!/usr/bin/env python3 --import asyncio --import sys -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport +@@ -6,10 +6,9 @@ --from third_party import X, Y, Z -+NOT_YET_IMPLEMENTED_StmtImportFrom - --from library import some_connection, some_decorator -+NOT_YET_IMPLEMENTED_StmtImportFrom + from library import some_connection, some_decorator # fmt: off -from third_party import (X, - Y, Z) -+NOT_YET_IMPLEMENTED_StmtImportFrom ++from third_party import X, Y, Z # fmt: on -f"trigger 3.6 mode" +NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -336,7 +327,7 @@ d={'a':1, # fmt: off - from hello import a, b - 'unformatted' -+ NOT_YET_IMPLEMENTED_StmtImportFrom ++ from hello import a, b + "unformatted" # fmt: on @@ -395,12 +386,8 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -150,12 +172,10 @@ - ast_args.kw_defaults, - parameters, - implicit_default=True, -- ) -+ ), +@@ -153,9 +175,7 @@ + ) ) # fmt: off - a = ( @@ -437,14 +424,14 @@ d={'a':1, ```py #!/usr/bin/env python3 -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport +import asyncio +import sys -NOT_YET_IMPLEMENTED_StmtImportFrom +from third_party import X, Y, Z -NOT_YET_IMPLEMENTED_StmtImportFrom +from library import some_connection, some_decorator # fmt: off -NOT_YET_IMPLEMENTED_StmtImportFrom +from third_party import X, Y, Z # fmt: on NOT_YET_IMPLEMENTED_ExprJoinedStr # Comment 1 @@ -543,7 +530,7 @@ def subscriptlist(): def import_as_names(): # fmt: off - NOT_YET_IMPLEMENTED_StmtImportFrom + from hello import a, b "unformatted" # fmt: on @@ -610,7 +597,7 @@ def long_lines(): ast_args.kw_defaults, parameters, implicit_default=True, - ), + ) ) # fmt: off a = unnecessary_bracket() diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap index 56b345fae1..abdfcb6d56 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap @@ -52,12 +52,7 @@ def test_calculate_fades(): ```diff --- Black +++ Ruff -@@ -1,40 +1,44 @@ --import pytest -+NOT_YET_IMPLEMENTED_StmtImport - - TmSt = 1 - TmEx = 2 +@@ -5,36 +5,40 @@ # fmt: off @@ -113,7 +108,7 @@ def test_calculate_fades(): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import pytest TmSt = 1 TmEx = 2 diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap index 89b2436af1..a8dc2ef620 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap @@ -37,20 +37,11 @@ def f(): pass + 2, + 3, + 4, -+ ], ++ ] +) # fmt: on def f(): pass -@@ -14,7 +18,7 @@ - 2, - 3, - 4, -- ] -+ ], - ) - def f(): - pass ``` ## Ruff Output @@ -63,7 +54,7 @@ def f(): pass 2, 3, 4, - ], + ] ) # fmt: on def f(): @@ -76,7 +67,7 @@ def f(): 2, 3, 4, - ], + ] ) def f(): pass diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap index 5cc5334320..9cd5e7ed31 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap @@ -103,7 +103,7 @@ elif unformatted: - # fmt: on - ] # Includes an formatted indentation. + # fmt: on -+ ], # Includes an formatted indentation. ++ ] # Includes an formatted indentation. }, ) @@ -186,7 +186,7 @@ setup( "foo-bar" "=foo.bar.:main", # fmt: on - ], # Includes an formatted indentation. + ] # Includes an formatted indentation. }, ) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap deleted file mode 100644 index 5a0bfd673f..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap +++ /dev/null @@ -1,114 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py ---- -## Input - -```py -# Regression test for https://github.com/psf/black/issues/3438 - -import ast -import collections # fmt: skip -import dataclasses -# fmt: off -import os -# fmt: on -import pathlib - -import re # fmt: skip -import secrets - -# fmt: off -import sys -# fmt: on - -import tempfile -import zoneinfo -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,19 +1,19 @@ - # Regression test for https://github.com/psf/black/issues/3438 - --import ast --import collections # fmt: skip --import dataclasses -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport # fmt: skip -+NOT_YET_IMPLEMENTED_StmtImport - # fmt: off --import os -+NOT_YET_IMPLEMENTED_StmtImport - # fmt: on --import pathlib -+NOT_YET_IMPLEMENTED_StmtImport - --import re # fmt: skip --import secrets -+NOT_YET_IMPLEMENTED_StmtImport # fmt: skip -+NOT_YET_IMPLEMENTED_StmtImport - - # fmt: off --import sys -+NOT_YET_IMPLEMENTED_StmtImport - # fmt: on - --import tempfile --import zoneinfo -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport -``` - -## Ruff Output - -```py -# Regression test for https://github.com/psf/black/issues/3438 - -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport # fmt: skip -NOT_YET_IMPLEMENTED_StmtImport -# fmt: off -NOT_YET_IMPLEMENTED_StmtImport -# fmt: on -NOT_YET_IMPLEMENTED_StmtImport - -NOT_YET_IMPLEMENTED_StmtImport # fmt: skip -NOT_YET_IMPLEMENTED_StmtImport - -# fmt: off -NOT_YET_IMPLEMENTED_StmtImport -# fmt: on - -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport -``` - -## Black Output - -```py -# Regression test for https://github.com/psf/black/issues/3438 - -import ast -import collections # fmt: skip -import dataclasses -# fmt: off -import os -# fmt: on -import pathlib - -import re # fmt: skip -import secrets - -# fmt: off -import sys -# fmt: on - -import tempfile -import zoneinfo -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap index eed13bbae7..3536f09059 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap @@ -107,20 +107,12 @@ def __await__(): return (yield) ```diff --- Black +++ Ruff -@@ -1,12 +1,11 @@ - #!/usr/bin/env python3 --import asyncio --import sys +@@ -5,8 +5,7 @@ + from third_party import X, Y, Z + + from library import some_connection, some_decorator - --from third_party import X, Y, Z -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport - --from library import some_connection, some_decorator -+NOT_YET_IMPLEMENTED_StmtImportFrom - -f"trigger 3.6 mode" -+NOT_YET_IMPLEMENTED_StmtImportFrom +NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -198,24 +190,6 @@ def __await__(): return (yield) def long_lines(): -@@ -87,7 +94,7 @@ - ast_args.kw_defaults, - parameters, - implicit_default=True, -- ) -+ ), - ) - typedargslist.extend( - gen_annotated_params( -@@ -96,7 +103,7 @@ - parameters, - implicit_default=True, - # trailing standalone comment -- ) -+ ), - ) - _type_comment_re = re.compile( - r""" @@ -135,14 +142,8 @@ a, **kwargs, @@ -239,12 +213,12 @@ def __await__(): return (yield) ```py #!/usr/bin/env python3 -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport +import asyncio +import sys -NOT_YET_IMPLEMENTED_StmtImportFrom +from third_party import X, Y, Z -NOT_YET_IMPLEMENTED_StmtImportFrom +from library import some_connection, some_decorator NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -334,7 +308,7 @@ def long_lines(): ast_args.kw_defaults, parameters, implicit_default=True, - ), + ) ) typedargslist.extend( gen_annotated_params( @@ -343,7 +317,7 @@ def long_lines(): parameters, implicit_default=True, # trailing standalone comment - ), + ) ) _type_comment_re = re.compile( r""" diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap index 3fd47e2167..23da8b6351 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap @@ -65,22 +65,15 @@ with hmm_but_this_should_get_two_preceding_newlines(): ```diff --- Black +++ Ruff -@@ -32,34 +32,28 @@ - - - if os.name == "posix": -- import termios -+ NOT_YET_IMPLEMENTED_StmtImport +@@ -36,7 +36,6 @@ def i_should_be_followed_by_only_one_newline(): pass - elif os.name == "nt": try: -- import msvcrt -+ NOT_YET_IMPLEMENTED_StmtImport - - def i_should_be_followed_by_only_one_newline(): + import msvcrt +@@ -45,21 +44,16 @@ pass except ImportError: @@ -141,13 +134,13 @@ def h(): if os.name == "posix": - NOT_YET_IMPLEMENTED_StmtImport + import termios def i_should_be_followed_by_only_one_newline(): pass elif os.name == "nt": try: - NOT_YET_IMPLEMENTED_StmtImport + import msvcrt def i_should_be_followed_by_only_one_newline(): pass diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap index 747b512442..fcb6ee100b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap @@ -73,15 +73,6 @@ some_module.some_function( ```diff --- Black +++ Ruff -@@ -27,7 +27,7 @@ - call( - arg={ - "explode": "this", -- } -+ }, - ) - call2( - arg=[1, 2, 3], @@ -35,7 +35,9 @@ x = { "a": 1, @@ -93,7 +84,7 @@ some_module.some_function( if ( a == { -@@ -47,22 +49,24 @@ +@@ -47,14 +49,16 @@ "f": 6, "g": 7, "h": 8, @@ -114,17 +105,6 @@ some_module.some_function( json = { "k": { "k2": { - "k3": [ - 1, -- ] -- } -- } -+ ], -+ }, -+ }, - } - - @@ -80,18 +84,14 @@ pass @@ -182,7 +162,7 @@ def f( call( arg={ "explode": "this", - }, + } ) call2( arg=[1, 2, 3], @@ -219,9 +199,9 @@ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ "k2": { "k3": [ 1, - ], - }, - }, + ] + } + } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap index 6b2e7df8f3..f08a097d01 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap @@ -61,79 +61,15 @@ __all__ = ( ```diff --- Black +++ Ruff -@@ -2,53 +2,31 @@ - - # flake8: noqa - --from logging import WARNING --from logging import ( -- ERROR, --) --import sys -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImport - - # This relies on each of the submodules having an __all__ variable. --from .base_events import * --from .coroutines import * --from .events import * # comment here -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom # comment here - --from .futures import * --from .locks import * # comment here --from .protocols import * -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -+NOT_YET_IMPLEMENTED_StmtImportFrom - --from ..runners import * # comment here --from ..queues import * --from ..streams import * -+NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom - --from some_library import ( -- Just, -- Enough, -- Libraries, -- To, -- Fit, -- In, -- This, -- Nice, -- Split, -- Which, -- We, -- No, -- Longer, -- Use, --) --from name_of_a_company.extremely_long_project_name.component.ttypes import ( +@@ -38,7 +38,7 @@ + Use, + ) + from name_of_a_company.extremely_long_project_name.component.ttypes import ( - CuteLittleServiceHandlerFactoryyy, --) --from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom ++ CuteLittleServiceHandlerFactoryyy + ) + from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * --from .a.b.c.subprocess import * --from . import tasks --from . import A, B, C --from . import ( -- SomeVeryLongNameAndAllOfItsAdditionalLetters1, -- SomeVeryLongNameAndAllOfItsAdditionalLetters2, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom - - __all__ = ( - base_events.__all__ ``` ## Ruff Output @@ -143,31 +79,53 @@ __all__ = ( # flake8: noqa -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImport +from logging import WARNING +from logging import ( + ERROR, +) +import sys # This relies on each of the submodules having an __all__ variable. -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom # comment here +from .base_events import * +from .coroutines import * +from .events import * # comment here -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -NOT_YET_IMPLEMENTED_StmtImportFrom +from .futures import * +from .locks import * # comment here +from .protocols import * -NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from ..runners import * # comment here +from ..queues import * +from ..streams import * -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from some_library import ( + Just, + Enough, + Libraries, + To, + Fit, + In, + This, + Nice, + Split, + Which, + We, + No, + Longer, + Use, +) +from name_of_a_company.extremely_long_project_name.component.ttypes import ( + CuteLittleServiceHandlerFactoryyy +) +from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from .a.b.c.subprocess import * +from . import tasks +from . import A, B, C +from . import ( + SomeVeryLongNameAndAllOfItsAdditionalLetters1, + SomeVeryLongNameAndAllOfItsAdditionalLetters2, +) __all__ = ( base_events.__all__ diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap index 2a803075f6..40cab8fa6b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap @@ -87,7 +87,7 @@ return np.divide( i = funcs.f() ** 5 j = super().name ** 5 -k = [(2**idx, value) for idx, value in pairs] -+k = [i for i in []] ++k = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) m = [([2**63], [1, 2**63])] n = count <= 10**5 @@ -95,7 +95,7 @@ return np.divide( -p = {(k, k**2): v**2 for k, v in pairs} -q = [10**i for i in range(6)] +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+q = [i for i in []] ++q = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] r = x**y a = 5.0**~4.0 @@ -110,7 +110,7 @@ return np.divide( i = funcs.f() ** 5.0 j = super().name ** 5.0 -k = [(2.0**idx, value) for idx, value in pairs] -+k = [i for i in []] ++k = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) m = [([2.0**63.0], [1.0, 2**63.0])] n = count <= 10**5.0 @@ -118,7 +118,7 @@ return np.divide( -p = {(k, k**2): v**2.0 for k, v in pairs} -q = [10.5**i for i in range(6)] +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+q = [i for i in []] ++q = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) @@ -164,13 +164,13 @@ g = a.b**c.d h = 5 ** funcs.f() i = funcs.f() ** 5 j = super().name ** 5 -k = [i for i in []] +k = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) m = [([2**63], [1, 2**63])] n = count <= 10**5 o = settings(max_examples=10**6) p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -q = [i for i in []] +q = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] r = x**y a = 5.0**~4.0 @@ -183,13 +183,13 @@ g = a.b**c.d h = 5.0 ** funcs.f() i = funcs.f() ** 5.0 j = super().name ** 5.0 -k = [i for i in []] +k = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) m = [([2.0**63.0], [1.0, 2**63.0])] n = count <= 10**5.0 o = settings(max_examples=10**6.0) p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -q = [i for i in []] +q = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap index e77d6934e3..d529f18f49 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap @@ -93,12 +93,6 @@ async def main(): ```diff --- Black +++ Ruff -@@ -1,4 +1,4 @@ --import asyncio -+NOT_YET_IMPLEMENTED_StmtImport - - - # Control example @@ -8,59 +8,70 @@ # Remove brackets for short coroutine/task @@ -219,7 +213,7 @@ async def main(): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import asyncio # Control example diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap index f9095f8b6f..c97f2a11d6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap @@ -120,12 +120,6 @@ with open("/path/to/file.txt", mode="r") as read_file: ```diff --- Black +++ Ruff -@@ -1,4 +1,4 @@ --import random -+NOT_YET_IMPLEMENTED_StmtImport - - - def foo1(): @@ -27,16 +27,16 @@ @@ -151,7 +145,7 @@ with open("/path/to/file.txt", mode="r") as read_file: ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import random def foo1(): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap index bf646fcda1..cfa0bebf04 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap @@ -43,7 +43,7 @@ ham[lower + offset : upper + offset] ```diff --- Black +++ Ruff -@@ -4,28 +4,28 @@ +@@ -4,28 +4,34 @@ slice[d::d] slice[0] slice[-1] @@ -65,13 +65,19 @@ ham[lower + offset : upper + offset] slice[not so_simple : 1 < val <= 10] -slice[(1 for i in range(42)) : x] -slice[:: [i for i in range(42)]] -+slice[(i for i in []) : x] -+slice[ :: [i for i in []]] ++slice[ ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) : x ++] ++slice[ ++ :: [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++] async def f(): - slice[await x : [i async for i in arange(42)] : 42] -+ slice[await x : [i for i in []] : 42] ++ slice[ ++ await x : [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] : 42 ++ ] # These are from PEP-8: @@ -103,12 +109,18 @@ slice[lambda x: True : lambda x: True] slice[lambda x: True :, None::] slice[1 or 2 : True and False] slice[not so_simple : 1 < val <= 10] -slice[(i for i in []) : x] -slice[ :: [i for i in []]] +slice[ + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) : x +] +slice[ + :: [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +] async def f(): - slice[await x : [i for i in []] : 42] + slice[ + await x : [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] : 42 + ] # These are from PEP-8: diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap index bbff73657c..2b48c542e5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap @@ -111,7 +111,7 @@ x53 = ( ## Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from argparse import Namespace a = Namespace() diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap index 8b6f78bd10..40a69332f5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap @@ -277,8 +277,14 @@ aaaaaaaaaaaaaa + { dddddddddddddddd, eeeeeee, } -aaaaaaaaaaaaaa + [i for i in []] -aaaaaaaaaaaaaa + (i for i in []) +( + aaaaaaaaaaaaaa + + [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +) +( + aaaaaaaaaaaaaa + + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +) aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} # Wraps it in parentheses if it needs to break both left and right @@ -293,9 +299,9 @@ not (aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_se [ a + [ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ] - in c, + in c ] diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap similarity index 83% rename from crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap index f59509126f..f8a09bc04e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py --- ## Input ```py @@ -87,11 +87,16 @@ f( dict() ) +# Don't add a magic trailing comma when there is only one entry +# Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 +f( + a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() +) ``` ## Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from unittest.mock import MagicMock def f(*args, **kwargs): @@ -111,10 +116,10 @@ f(x=2) f(1, x=2) f( - this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd, + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd ) f( - this_is_a_very_long_keyword_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd=1, + this_is_a_very_long_keyword_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd=1 ) f( @@ -168,6 +173,12 @@ f( **dict(), # oddly placed own line comment ) + +# Don't add a magic trailing comma when there is only one entry +# Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 +f( + a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() +) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap index 6031fbadf8..03d75d057b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap @@ -180,10 +180,10 @@ return ( ( a + [ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ] >= c - ), + ) ] ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap index 25a854128c..bfafb3e2e7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap @@ -69,7 +69,7 @@ a = { # before { # open - key: value, # key # colon # value + key: value # key # colon # value } # close # after @@ -82,7 +82,7 @@ a = { } { - **b, # middle with single item + **b # middle with single item } { diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap index 8930e0036c..e21436a052 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap @@ -14,6 +14,19 @@ a2 = [ # a a3 = [ # b ] + +# Add magic trailing comma only if there is more than one entry, but respect it if it's +# already there +b1 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] +b2 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] +b3 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] ``` ## Output @@ -28,6 +41,19 @@ a2 = [ # a a3 = [ # b ] + +# Add magic trailing comma only if there is more than one entry, but respect it if it's +# already there +b1 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] +b2 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] +b3 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap new file mode 100644 index 0000000000..0208606383 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py +--- +## Input +```py +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd +``` + +## Output +```py +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__import_from.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__import_from.py.snap new file mode 100644 index 0000000000..0d8c90572e --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__import_from.py.snap @@ -0,0 +1,46 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py +--- +## Input +```py +from a import aksjdhflsakhdflkjsadlfajkslhf +from a import ( + aksjdhflsakhdflkjsadlfajkslhf, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa import * +``` + +## Output +```py +from a import aksjdhflsakhdflkjsadlfajkslhf +from a import ( + aksjdhflsakhdflkjsadlfajkslhf, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa import * +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap index 8f54c3461d..52be9741ad 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap @@ -113,15 +113,13 @@ with a: # should remove brackets # if we do want to wrap, do we prefer to wrap the entire WithItem or to let the # WithItem allow the `aa + bb` content expression to be wrapped with ( - ( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ) as c, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c ): ... # currently unparsable by black: https://github.com/psf/black/issues/3678 -with (i for i in []): +with (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): pass with (a, *NOT_YET_IMPLEMENTED_ExprStarred): pass diff --git a/crates/ruff_python_resolver/src/implicit_imports.rs b/crates/ruff_python_resolver/src/implicit_imports.rs index 94bf9a9f2c..693b6572ca 100644 --- a/crates/ruff_python_resolver/src/implicit_imports.rs +++ b/crates/ruff_python_resolver/src/implicit_imports.rs @@ -80,7 +80,7 @@ impl ImplicitImports { continue; }; - let Some(name) = path.file_name().and_then(OsStr::to_str) else { + let Some(name) = path.file_name().and_then(OsStr::to_str) else { continue; }; submodules.insert( diff --git a/crates/ruff_python_semantic/src/analyze/function_type.rs b/crates/ruff_python_semantic/src/analyze/function_type.rs index 63f9b8ce71..95b0f3845e 100644 --- a/crates/ruff_python_semantic/src/analyze/function_type.rs +++ b/crates/ruff_python_semantic/src/analyze/function_type.rs @@ -6,10 +6,7 @@ use ruff_python_ast::helpers::map_callable; use crate::model::SemanticModel; use crate::scope::{Scope, ScopeKind}; -const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"]; -const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")]; - -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub enum FunctionType { Function, Method, @@ -44,24 +41,28 @@ pub fn classify( }) }) { FunctionType::StaticMethod - } else if CLASS_METHODS.contains(&name) - // Special-case class method, like `__new__`. + } else if matches!(name, "__new__" | "__init_subclass__" | "__class_getitem__") + // Special-case class method, like `__new__`. || scope.bases.iter().any(|expr| { // The class itself extends a known metaclass, so all methods are class methods. - semantic.resolve_call_path(map_callable(expr)).map_or(false, |call_path| { - METACLASS_BASES - .iter() - .any(|(module, member)| call_path.as_slice() == [*module, *member]) - }) + semantic + .resolve_call_path(map_callable(expr)) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "type"] | ["abc", "ABCMeta"]) + }) }) || decorator_list.iter().any(|decorator| { // The method is decorated with a class method decorator (like `@classmethod`). - semantic.resolve_call_path(map_callable(&decorator.expression)).map_or(false, |call_path| { - matches!(call_path.as_slice(), ["", "classmethod"] | ["abc", "abstractclassmethod"]) || - classmethod_decorators - .iter() - .any(|decorator| call_path == from_qualified_name(decorator)) - }) + semantic + .resolve_call_path(map_callable(&decorator.expression)) + .map_or(false, |call_path| { + matches!( + call_path.as_slice(), + ["", "classmethod"] | ["abc", "abstractclassmethod"] + ) || classmethod_decorators + .iter() + .any(|decorator| call_path == from_qualified_name(decorator)) + }) }) { FunctionType::ClassMethod diff --git a/crates/ruff_python_semantic/src/analyze/logging.rs b/crates/ruff_python_semantic/src/analyze/logging.rs index a96ebad213..48fbc3fb16 100644 --- a/crates/ruff_python_semantic/src/analyze/logging.rs +++ b/crates/ruff_python_semantic/src/analyze/logging.rs @@ -20,7 +20,11 @@ use crate::model::SemanticModel; pub fn is_logger_candidate(func: &Expr, semantic: &SemanticModel) -> bool { if let Expr::Attribute(ast::ExprAttribute { value, .. }) = func { let Some(call_path) = (if let Some(call_path) = semantic.resolve_call_path(value) { - if call_path.first().map_or(false, |module| *module == "logging") || call_path.as_slice() == ["flask", "current_app", "logger"] { + if call_path + .first() + .map_or(false, |module| *module == "logging") + || call_path.as_slice() == ["flask", "current_app", "logger"] + { Some(call_path) } else { None diff --git a/crates/ruff_python_stdlib/src/identifiers.rs b/crates/ruff_python_stdlib/src/identifiers.rs index 2dca484a3c..169959bee2 100644 --- a/crates/ruff_python_stdlib/src/identifiers.rs +++ b/crates/ruff_python_stdlib/src/identifiers.rs @@ -1,4 +1,4 @@ -use crate::keyword::KWLIST; +use crate::keyword::is_keyword; /// Returns `true` if a string is a valid Python identifier (e.g., variable /// name). @@ -18,7 +18,7 @@ pub fn is_identifier(name: &str) -> bool { } // Is the identifier a keyword? - if KWLIST.contains(&name) { + if is_keyword(name) { return false; } @@ -52,7 +52,7 @@ pub fn is_module_name(name: &str) -> bool { } // Is the identifier a keyword? - if KWLIST.contains(&name) { + if is_keyword(name) { return false; } @@ -70,7 +70,7 @@ pub fn is_migration_name(name: &str) -> bool { } // Is the identifier a keyword? - if KWLIST.contains(&name) { + if is_keyword(name) { return false; } diff --git a/crates/ruff_python_stdlib/src/keyword.rs b/crates/ruff_python_stdlib/src/keyword.rs index ddaec03c80..7f361c0b69 100644 --- a/crates/ruff_python_stdlib/src/keyword.rs +++ b/crates/ruff_python_stdlib/src/keyword.rs @@ -1,7 +1,41 @@ // See: https://github.com/python/cpython/blob/9d692841691590c25e6cf5b2250a594d3bf54825/Lib/keyword.py#L18 -pub(crate) const KWLIST: [&str; 35] = [ - "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", - "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", - "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", - "with", "yield", -]; +pub(crate) fn is_keyword(name: &str) -> bool { + matches!( + name, + "False" + | "None" + | "True" + | "and" + | "as" + | "assert" + | "async" + | "await" + | "break" + | "class" + | "continue" + | "def" + | "del" + | "elif" + | "else" + | "except" + | "finally" + | "for" + | "from" + | "global" + | "if" + | "import" + | "in" + | "is" + | "lambda" + | "nonlocal" + | "not" + | "or" + | "pass" + | "raise" + | "return" + | "try" + | "while" + | "with" + | "yield", + ) +} diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 1fe7aa89d6..27e3de73c0 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -13,7 +13,7 @@ use ruff::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use ruff::settings::configuration::Configuration; use ruff::settings::options::Options; @@ -166,6 +166,7 @@ pub fn defaultSettings() -> Result { pydocstyle: Some(pydocstyle::settings::Settings::default().into()), pyflakes: Some(pyflakes::settings::Settings::default().into()), pylint: Some(pylint::settings::Settings::default().into()), + pyupgrade: Some(pyupgrade::settings::Settings::default().into()), })?) } diff --git a/docs/tutorial.md b/docs/tutorial.md index e9f6592061..452bb2583e 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -242,7 +242,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.275 + rev: v0.0.276 hooks: - id: ruff ``` diff --git a/docs/usage.md b/docs/usage.md index 8528ec9142..4d2f32448f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -22,7 +22,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.275 + rev: v0.0.276 hooks: - id: ruff ``` @@ -32,7 +32,7 @@ Or, to enable autofix: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.275 + rev: v0.0.276 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/pyproject.toml b/pyproject.toml index e5e55e84ef..b860e31629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.0.275" +version = "0.0.276" description = "An extremely fast Python linter, written in Rust." authors = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }] maintainers = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }] diff --git a/ruff.schema.json b/ruff.schema.json index c6d3d474b7..064c0f7ec9 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -475,6 +475,17 @@ } ] }, + "pyupgrade": { + "description": "Options for the `pyupgrade` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/PyUpgradeOptions" + }, + { + "type": "null" + } + ] + }, "required-version": { "description": "Require a specific version of Ruff to be running (useful for unifying results across many environments, e.g., with a `pyproject.toml` file).", "anyOf": [ @@ -1419,6 +1430,19 @@ }, "additionalProperties": false }, + "PyUpgradeOptions": { + "type": "object", + "properties": { + "keep-runtime-typing": { + "description": "Whether to avoid PEP 585 (`List[int]` -> `list[int]`) and PEP 604 (`Union[str, int]` -> `str | int`) rewrites even if a file imports `from __future__ import annotations`.\n\nThis setting is only applicable when the target Python version is below 3.9 and 3.10 respectively, and is most commonly used when working with libraries like Pydantic and FastAPI, which rely on the ability to parse type annotations at runtime. The use of `from __future__ import annotations` causes Python to treat the type annotations as strings, which typically allows for the use of language features that appear in later Python versions but are not yet supported by the current version (e.g., `str | int`). However, libraries that rely on runtime type annotations will break if the annotations are incompatible with the current Python version.\n\nFor example, while the following is valid Python 3.8 code due to the presence of `from __future__ import annotations`, the use of `str| int` prior to Python 3.10 will cause Pydantic to raise a `TypeError` at runtime:\n\n```python from __future__ import annotations\n\nimport pydantic\n\nclass Foo(pydantic.BaseModel): bar: str | int ```", + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + }, "Pycodestyle": { "type": "object", "properties": { @@ -1561,19 +1585,19 @@ }, "Quote": { "oneOf": [ - { - "description": "Use single quotes.", - "type": "string", - "enum": [ - "single" - ] - }, { "description": "Use double quotes.", "type": "string", "enum": [ "double" ] + }, + { + "description": "Use single quotes.", + "type": "string", + "enum": [ + "single" + ] } ] }, @@ -2042,6 +2066,7 @@ "NPY00", "NPY001", "NPY002", + "NPY003", "PD", "PD0", "PD00", @@ -2068,6 +2093,10 @@ "PERF2", "PERF20", "PERF203", + "PERF4", + "PERF40", + "PERF401", + "PERF402", "PGH", "PGH0", "PGH00", @@ -2283,6 +2312,10 @@ "PYI0", "PYI00", "PYI001", + "PYI002", + "PYI003", + "PYI004", + "PYI005", "PYI006", "PYI007", "PYI008", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index be2ef93a46..f10e38cb0e 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -186,10 +186,7 @@ def main(argv: Sequence[str] | None = None) -> int: generate_docs() # Get static docs - static_docs = [] - for file in os.listdir("docs"): - if file.endswith(".md"): - static_docs.append(Path("docs") / file) + static_docs = [Path("docs") / f for f in os.listdir("docs") if f.endswith(".md")] # Check rules generated if not Path("docs/rules").exists(): @@ -197,10 +194,9 @@ def main(argv: Sequence[str] | None = None) -> int: return 1 # Get generated rules - generated_docs = [] - for file in os.listdir("docs/rules"): - if file.endswith(".md"): - generated_docs.append(Path("docs/rules") / file) + generated_docs = [ + Path("docs/rules") / f for f in os.listdir("docs/rules") if f.endswith(".md") + ] if len(generated_docs) == 0: print("Please generate rules first.") diff --git a/scripts/update_ambiguous_characters.py b/scripts/update_ambiguous_characters.py index 27a06fd039..cf165af585 100644 --- a/scripts/update_ambiguous_characters.py +++ b/scripts/update_ambiguous_characters.py @@ -45,9 +45,7 @@ def format_confusables_rs(raw_data: dict[str, list[int]]) -> str: for i in range(0, len(items), 2): flattened_items.add((items[i], items[i + 1])) - tuples = [] - for left, right in sorted(flattened_items): - tuples.append(f" {left}u32 => {right},\n") + tuples = [f" {left}u32 => {right},\n" for left, right in sorted(flattened_items)] print(f"{len(tuples)} confusable tuples.")