From 18d2b2f0eee1de78890837a8dbcc97fabda411d0 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Sat, 11 Oct 2025 10:05:01 +0200 Subject: [PATCH] Skip fix offering for concatenated stringinzed annotations Similar to RUF013 --- .../test/fixtures/flake8_pyi/PYI041_3.py | 25 +++++ .../test/fixtures/flake8_pyi/PYI041_4.py | 8 ++ .../ruff_linter/src/rules/flake8_pyi/mod.rs | 1 + .../rules/redundant_numeric_union.rs | 22 +++-- ...flake8_pyi__tests__PYI041_PYI041_3.py.snap | 98 ++++++++++++++++--- ...flake8_pyi__tests__PYI041_PYI041_4.py.snap | 70 +++++++++++++ ..._ruff__tests__PY39_RUF013_RUF013_0.py.snap | 1 - ...ules__ruff__tests__RUF013_RUF013_0.py.snap | 1 - 8 files changed, 198 insertions(+), 28 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_4.py create mode 100644 crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_4.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_3.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_3.py index 79cd0f2ce6..42d44756b6 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_3.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_3.py @@ -110,3 +110,28 @@ class Issue18298: def f3(self, arg: "None | float | None | int | None" = None) -> "None": # PYI041 - with fix pass + + +class FooStringConcat: + def good(self, arg: "i" "nt") -> "None": + ... + + def bad(self, arg: "int " "| float | com" "plex") -> "None": + ... + + def bad2(self, arg: "int | Union[flo" "at, complex]") -> "None": + ... + + def bad3(self, arg: "Union[Union[float, com" "plex], int]") -> "None": + ... + + def bad4(self, arg: "Union[float | complex, in" "t ]") -> "None": + ... + + def bad5(self, arg: "int | " + "(float | complex)") -> "None": + ... + + def bad6(self, arg: "in\ +t | (float | compl" "ex)") -> "None": + ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_4.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_4.py new file mode 100644 index 0000000000..d0e3467a1e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_4.py @@ -0,0 +1,8 @@ +from typing import Union as Uno + + +def f1(a: "U" "no[int, fl" "oat, Foo]") -> "None": ... +def f2(a: "Uno[int, float, Foo]") -> "None": ... +def f3(a: """Uno[int, float, Foo]""") -> "None": ... +def f4(a: "Uno[in\ +t, float, Foo]") -> "None": ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/mod.rs b/crates/ruff_linter/src/rules/flake8_pyi/mod.rs index 1c0c0408ac..fecc7836b0 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/mod.rs @@ -77,6 +77,7 @@ mod tests { #[test_case(Rule::RedundantNumericUnion, Path::new("PYI041_1.pyi"))] #[test_case(Rule::RedundantNumericUnion, Path::new("PYI041_2.py"))] #[test_case(Rule::RedundantNumericUnion, Path::new("PYI041_3.py"))] + #[test_case(Rule::RedundantNumericUnion, Path::new("PYI041_4.py"))] #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.py"))] #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))] #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.py"))] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs index 27b37e2e36..fc5ad100f9 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs @@ -143,18 +143,20 @@ fn check_annotation<'a>(checker: &Checker, annotation: &'a Expr, unresolved_anno return; } - let string_annotation = unresolved_annotation - .as_string_literal_expr() - .map(|str| str.value.to_str()); + let string_annotation = unresolved_annotation.as_string_literal_expr(); + if string_annotation.is_some_and(|s| s.value.is_implicit_concatenated()) { + // No fix for concatenated string literals. They're rare and too complex to handle. + // https://github.com/astral-sh/ruff/issues/19184#issuecomment-3047695205 + return; + } // Mark [`Fix`] as unsafe when comments are in range. - let applicability = if string_annotation.is_some_and(|s| s.contains('#')) - || checker.comment_ranges().intersects(annotation.range()) - { - Applicability::Unsafe - } else { - Applicability::Safe - }; + let applicability = + if string_annotation.is_some() || checker.comment_ranges().intersects(annotation.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }; // Generate the flattened fix once. let fix = if let &[edit_expr] = necessary_nodes.as_slice() { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_3.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_3.py.snap index b88edb0a6e..23e8ec7631 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_3.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_3.py.snap @@ -9,7 +9,7 @@ PYI041_3.py:23:15: PYI041 [*] Use `float` instead of `int | float` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 20 20 | ... 21 21 | 22 22 | @@ -27,7 +27,7 @@ PYI041_3.py:27:33: PYI041 [*] Use `complex` instead of `float | complex` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 24 24 | ... 25 25 | 26 26 | @@ -45,7 +45,7 @@ PYI041_3.py:31:31: PYI041 [*] Use `float` instead of `int | float` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 28 28 | ... 29 29 | 30 30 | @@ -63,7 +63,7 @@ PYI041_3.py:35:29: PYI041 [*] Use `float` instead of `int | float` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 32 32 | ... 33 33 | 34 34 | @@ -81,7 +81,7 @@ PYI041_3.py:39:25: PYI041 [*] Use `float` instead of `int | float` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 36 36 | ... 37 37 | 38 38 | @@ -99,7 +99,7 @@ PYI041_3.py:43:29: PYI041 [*] Use `float` instead of `int | float` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 40 40 | ... 41 41 | 42 42 | @@ -117,7 +117,7 @@ PYI041_3.py:47:29: PYI041 [*] Use `float` instead of `int | float` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 44 44 | ... 45 45 | 46 46 | @@ -135,7 +135,7 @@ PYI041_3.py:51:29: PYI041 [*] Use `float` instead of `int | float` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 48 48 | ... 49 49 | 50 50 | @@ -153,7 +153,7 @@ PYI041_3.py:55:29: PYI041 [*] Use `float` instead of `int | float` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 52 52 | ... 53 53 | 54 54 | @@ -221,7 +221,7 @@ PYI041_3.py:80:25: PYI041 [*] Use `complex` instead of `int | float | complex` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 77 77 | def good(self, arg: "int") -> "None": 78 78 | ... 79 79 | @@ -241,7 +241,7 @@ PYI041_3.py:83:26: PYI041 [*] Use `complex` instead of `int | float | complex` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 80 80 | def bad(self, arg: "int | float | complex") -> "None": 81 81 | ... 82 82 | @@ -261,7 +261,7 @@ PYI041_3.py:86:26: PYI041 [*] Use `complex` instead of `int | float | complex` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 83 83 | def bad2(self, arg: "int | Union[float, complex]") -> "None": 84 84 | ... 85 85 | @@ -281,7 +281,7 @@ PYI041_3.py:89:26: PYI041 [*] Use `complex` instead of `int | float | complex` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 86 86 | def bad3(self, arg: "Union[Union[float, complex], int]") -> "None": 87 87 | ... 88 88 | @@ -301,7 +301,7 @@ PYI041_3.py:92:26: PYI041 [*] Use `complex` instead of `int | float | complex` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 89 89 | def bad4(self, arg: "Union[float | complex, int]") -> "None": 90 90 | ... 91 91 | @@ -332,7 +332,7 @@ PYI041_3.py:104:28: PYI041 [*] Use `float` instead of `int | float` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 101 101 | 102 102 | if TYPE_CHECKING: 103 103 | @@ -352,10 +352,76 @@ PYI041_3.py:111:24: PYI041 [*] Use `float` instead of `int | float` | = help: Remove redundant type -ℹ Safe fix +ℹ Unsafe fix 108 108 | def f2(self, arg=None) -> "None": 109 109 | pass 110 110 | 111 |- def f3(self, arg: "None | float | None | int | None" = None) -> "None": # PYI041 - with fix 111 |+ def f3(self, arg: "None | float | None | None" = None) -> "None": # PYI041 - with fix 112 112 | pass +113 113 | +114 114 | + +PYI041_3.py:119:24: PYI041 Use `complex` instead of `int | float | complex` + | +117 | ... +118 | +119 | def bad(self, arg: "int " "| float | com" "plex") -> "None": + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +120 | ... + | + = help: Remove redundant type + +PYI041_3.py:122:25: PYI041 Use `complex` instead of `int | float | complex` + | +120 | ... +121 | +122 | def bad2(self, arg: "int | Union[flo" "at, complex]") -> "None": + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +123 | ... + | + = help: Remove redundant type + +PYI041_3.py:125:25: PYI041 Use `complex` instead of `int | float | complex` + | +123 | ... +124 | +125 | def bad3(self, arg: "Union[Union[float, com" "plex], int]") -> "None": + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +126 | ... + | + = help: Remove redundant type + +PYI041_3.py:128:25: PYI041 Use `complex` instead of `int | float | complex` + | +126 | ... +127 | +128 | def bad4(self, arg: "Union[float | complex, in" "t ]") -> "None": + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +129 | ... + | + = help: Remove redundant type + +PYI041_3.py:131:25: PYI041 Use `complex` instead of `int | float | complex` + | +129 | ... +130 | +131 | def bad5(self, arg: "int | " + | _________________________^ +132 | | "(float | complex)") -> "None": + | |___________________________________________^ PYI041 +133 | ... + | + = help: Remove redundant type + +PYI041_3.py:135:25: PYI041 Use `complex` instead of `int | float | complex` + | +133 | ... +134 | +135 | def bad6(self, arg: "in\ + | _________________________^ +136 | | t | (float | compl" "ex)") -> "None": + | |_________________________^ PYI041 +137 | ... + | + = help: Remove redundant type diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_4.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_4.py.snap new file mode 100644 index 0000000000..01b0ffccb9 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_4.py.snap @@ -0,0 +1,70 @@ +--- +source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs +--- +PYI041_4.py:4:11: PYI041 Use `float` instead of `int | float` + | +4 | def f1(a: "U" "no[int, fl" "oat, Foo]") -> "None": ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +5 | def f2(a: "Uno[int, float, Foo]") -> "None": ... +6 | def f3(a: """Uno[int, float, Foo]""") -> "None": ... + | + = help: Remove redundant type + +PYI041_4.py:5:12: PYI041 [*] Use `float` instead of `int | float` + | +4 | def f1(a: "U" "no[int, fl" "oat, Foo]") -> "None": ... +5 | def f2(a: "Uno[int, float, Foo]") -> "None": ... + | ^^^^^^^^^^^^^^^^^^^^ PYI041 +6 | def f3(a: """Uno[int, float, Foo]""") -> "None": ... +7 | def f4(a: "Uno[in\ + | + = help: Remove redundant type + +ℹ Unsafe fix +2 2 | +3 3 | +4 4 | def f1(a: "U" "no[int, fl" "oat, Foo]") -> "None": ... +5 |-def f2(a: "Uno[int, float, Foo]") -> "None": ... + 5 |+def f2(a: "Uno[float, Foo]") -> "None": ... +6 6 | def f3(a: """Uno[int, float, Foo]""") -> "None": ... +7 7 | def f4(a: "Uno[in\ +8 8 | t, float, Foo]") -> "None": ... + +PYI041_4.py:6:14: PYI041 [*] Use `float` instead of `int | float` + | +4 | def f1(a: "U" "no[int, fl" "oat, Foo]") -> "None": ... +5 | def f2(a: "Uno[int, float, Foo]") -> "None": ... +6 | def f3(a: """Uno[int, float, Foo]""") -> "None": ... + | ^^^^^^^^^^^^^^^^^^^^ PYI041 +7 | def f4(a: "Uno[in\ +8 | t, float, Foo]") -> "None": ... + | + = help: Remove redundant type + +ℹ Unsafe fix +3 3 | +4 4 | def f1(a: "U" "no[int, fl" "oat, Foo]") -> "None": ... +5 5 | def f2(a: "Uno[int, float, Foo]") -> "None": ... +6 |-def f3(a: """Uno[int, float, Foo]""") -> "None": ... + 6 |+def f3(a: """Uno[float, Foo]""") -> "None": ... +7 7 | def f4(a: "Uno[in\ +8 8 | t, float, Foo]") -> "None": ... + +PYI041_4.py:7:11: PYI041 [*] Use `float` instead of `int | float` + | +5 | def f2(a: "Uno[int, float, Foo]") -> "None": ... +6 | def f3(a: """Uno[int, float, Foo]""") -> "None": ... +7 | def f4(a: "Uno[in\ + | ___________^ +8 | | t, float, Foo]") -> "None": ... + | |_______________^ PYI041 + | + = help: Remove redundant type + +ℹ Unsafe fix +4 4 | def f1(a: "U" "no[int, fl" "oat, Foo]") -> "None": ... +5 5 | def f2(a: "Uno[int, float, Foo]") -> "None": ... +6 6 | def f3(a: """Uno[int, float, Foo]""") -> "None": ... +7 |-def f4(a: "Uno[in\ +8 |-t, float, Foo]") -> "None": ... + 7 |+def f4(a: Uno[float, Foo]) -> "None": ... diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap index 2617077336..096eceb46b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs -snapshot_kind: text --- RUF013_0.py:20:12: RUF013 [*] PEP 484 prohibits implicit `Optional` | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_0.py.snap index ded2a1770c..50bcc3df25 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_0.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs -snapshot_kind: text --- RUF013_0.py:20:12: RUF013 [*] PEP 484 prohibits implicit `Optional` |