This commit is contained in:
Robsdedude 2025-11-16 09:12:59 +01:00 committed by GitHub
commit 12aa7604ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 688 additions and 7 deletions

View file

@ -0,0 +1,137 @@
from typing import (
TYPE_CHECKING,
Union,
)
from typing_extensions import (
TypeAlias,
)
TA0: TypeAlias = "int"
TA1: TypeAlias = "int | float | bool"
TA2: TypeAlias = "Union[int, float, bool]"
def good1(arg: "int") -> "int | bool":
...
def good2(arg: "int", arg2: "int | bool") -> "None":
...
def f0(arg1: "float | int") -> "None":
...
def f1(arg1: "float", *, arg2: "float | list[str] | type[bool] | complex") -> "None":
...
def f2(arg1: "int", /, arg2: "int | int | float") -> "None":
...
def f3(arg1: "int", *args: "Union[int | int | float]") -> "None":
...
async def f4(**kwargs: "int | int | float") -> "None":
...
def f5(arg1: "int", *args: "Union[int, int, float]") -> "None":
...
def f6(arg1: "int", *args: "Union[Union[int, int, float]]") -> "None":
...
def f7(arg1: "int", *args: "Union[Union[Union[int, int, float]]]") -> "None":
...
def f8(arg1: "int", *args: "Union[Union[Union[int | int | float]]]") -> "None":
...
def f9(
arg: """Union[ # comment
float, # another
complex, int]"""
) -> "None":
...
def f10(
arg: """
int | # comment
float | # another
complex
"""
) -> "None":
...
class Foo:
def good(self, arg: "int") -> "None":
...
def bad(self, arg: "int | float | complex") -> "None":
...
def bad2(self, arg: "int | Union[float, complex]") -> "None":
...
def bad3(self, arg: "Union[Union[float, complex], int]") -> "None":
...
def bad4(self, arg: "Union[float | complex, int]") -> "None":
...
def bad5(self, arg: "int | (float | complex)") -> "None":
...
# https://github.com/astral-sh/ruff/issues/18298
# fix must not yield runtime `None | None | ...` (TypeError)
class Issue18298:
def f1(self, arg: "None | int | None | float" = None) -> "None": # PYI041 - no fix
pass
if TYPE_CHECKING:
def f2(self, arg: "None | int | None | float" = None) -> "None": ... # PYI041 - with fix
else:
def f2(self, arg=None) -> "None":
pass
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":
...

View file

@ -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": ...

View file

@ -543,6 +543,23 @@ impl<'a> Checker<'a> {
} }
} }
/// Given a type annotation [`Expr`], abstracting over the fact that the annotation expression
/// might be "stringized".
///
/// A stringized annotation is one enclosed in string quotes:
/// `foo: "typing.Any"` means the same thing to a type checker as `foo: typing.Any`.
pub(crate) fn map_maybe_stringized_annotation<'b>(&self, expr: &'b ast::Expr) -> &'b ast::Expr
where
'a: 'b,
{
if let ast::Expr::StringLiteral(string_annotation) = expr {
if let Ok(parsed_annotation) = self.parse_type_annotation(string_annotation) {
return parsed_annotation.expression();
}
}
expr
}
/// Push `diagnostic` if the checker is not in a `@no_type_check` context. /// Push `diagnostic` if the checker is not in a `@no_type_check` context.
pub(crate) fn report_type_diagnostic<T: Violation>(&self, kind: T, range: TextRange) { pub(crate) fn report_type_diagnostic<T: Violation>(&self, kind: T, range: TextRange) {
if !self.semantic.in_no_type_check() { if !self.semantic.in_no_type_check() {

View file

@ -76,6 +76,8 @@ mod tests {
#[test_case(Rule::RedundantNumericUnion, Path::new("PYI041_1.py"))] #[test_case(Rule::RedundantNumericUnion, Path::new("PYI041_1.py"))]
#[test_case(Rule::RedundantNumericUnion, Path::new("PYI041_1.pyi"))] #[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_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.py"))]
#[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))] #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))]
#[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.py"))] #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.py"))]

View file

@ -81,11 +81,15 @@ impl Violation for RedundantNumericUnion {
/// PYI041 /// PYI041
pub(crate) fn redundant_numeric_union(checker: &Checker, parameters: &Parameters) { pub(crate) fn redundant_numeric_union(checker: &Checker, parameters: &Parameters) {
for annotation in parameters.iter().filter_map(AnyParameterRef::annotation) { for annotation in parameters.iter().filter_map(AnyParameterRef::annotation) {
check_annotation(checker, annotation); check_annotation(
checker,
checker.map_maybe_stringized_annotation(annotation),
annotation,
);
} }
} }
fn check_annotation<'a>(checker: &Checker, annotation: &'a Expr) { fn check_annotation<'a>(checker: &Checker, annotation: &'a Expr, unresolved_annotation: &'a Expr) {
let mut numeric_flags = NumericFlags::empty(); let mut numeric_flags = NumericFlags::empty();
let mut find_numeric_type = |expr: &Expr, _parent: &Expr| { let mut find_numeric_type = |expr: &Expr, _parent: &Expr| {
@ -142,12 +146,20 @@ fn check_annotation<'a>(checker: &Checker, annotation: &'a Expr) {
return; return;
} }
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. // Mark [`Fix`] as unsafe when comments are in range.
let applicability = if checker.comment_ranges().intersects(annotation.range()) { let applicability =
Applicability::Unsafe if string_annotation.is_some() || checker.comment_ranges().intersects(annotation.range()) {
} else { Applicability::Unsafe
Applicability::Safe } else {
}; Applicability::Safe
};
// Generate the flattened fix once. // Generate the flattened fix once.
let fix = if let &[edit_expr] = necessary_nodes.as_slice() { let fix = if let &[edit_expr] = necessary_nodes.as_slice() {

View file

@ -0,0 +1,434 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_3.py:23:15
|
23 | def f0(arg1: "float | int") -> "None":
| ^^^^^^^^^^^
24 | ...
|
help: Remove redundant type
20 | ...
21 |
22 |
- def f0(arg1: "float | int") -> "None":
23 + def f0(arg1: "float") -> "None":
24 | ...
25 |
26 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `complex` instead of `float | complex`
--> PYI041_3.py:27:33
|
27 | def f1(arg1: "float", *, arg2: "float | list[str] | type[bool] | complex") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | ...
|
help: Remove redundant type
24 | ...
25 |
26 |
- def f1(arg1: "float", *, arg2: "float | list[str] | type[bool] | complex") -> "None":
27 + def f1(arg1: "float", *, arg2: "list[str] | type[bool] | complex") -> "None":
28 | ...
29 |
30 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_3.py:31:31
|
31 | def f2(arg1: "int", /, arg2: "int | int | float") -> "None":
| ^^^^^^^^^^^^^^^^^
32 | ...
|
help: Remove redundant type
28 | ...
29 |
30 |
- def f2(arg1: "int", /, arg2: "int | int | float") -> "None":
31 + def f2(arg1: "int", /, arg2: "float") -> "None":
32 | ...
33 |
34 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_3.py:35:29
|
35 | def f3(arg1: "int", *args: "Union[int | int | float]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^
36 | ...
|
help: Remove redundant type
32 | ...
33 |
34 |
- def f3(arg1: "int", *args: "Union[int | int | float]") -> "None":
35 + def f3(arg1: "int", *args: "float") -> "None":
36 | ...
37 |
38 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_3.py:39:25
|
39 | async def f4(**kwargs: "int | int | float") -> "None":
| ^^^^^^^^^^^^^^^^^
40 | ...
|
help: Remove redundant type
36 | ...
37 |
38 |
- async def f4(**kwargs: "int | int | float") -> "None":
39 + async def f4(**kwargs: "float") -> "None":
40 | ...
41 |
42 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_3.py:43:29
|
43 | def f5(arg1: "int", *args: "Union[int, int, float]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^
44 | ...
|
help: Remove redundant type
40 | ...
41 |
42 |
- def f5(arg1: "int", *args: "Union[int, int, float]") -> "None":
43 + def f5(arg1: "int", *args: "float") -> "None":
44 | ...
45 |
46 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_3.py:47:29
|
47 | def f6(arg1: "int", *args: "Union[Union[int, int, float]]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
48 | ...
|
help: Remove redundant type
44 | ...
45 |
46 |
- def f6(arg1: "int", *args: "Union[Union[int, int, float]]") -> "None":
47 + def f6(arg1: "int", *args: "float") -> "None":
48 | ...
49 |
50 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_3.py:51:29
|
51 | def f7(arg1: "int", *args: "Union[Union[Union[int, int, float]]]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | ...
|
help: Remove redundant type
48 | ...
49 |
50 |
- def f7(arg1: "int", *args: "Union[Union[Union[int, int, float]]]") -> "None":
51 + def f7(arg1: "int", *args: "float") -> "None":
52 | ...
53 |
54 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_3.py:55:29
|
55 | def f8(arg1: "int", *args: "Union[Union[Union[int | int | float]]]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
56 | ...
|
help: Remove redundant type
52 | ...
53 |
54 |
- def f8(arg1: "int", *args: "Union[Union[Union[int | int | float]]]") -> "None":
55 + def f8(arg1: "int", *args: "float") -> "None":
56 | ...
57 |
58 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `complex` instead of `int | float | complex`
--> PYI041_3.py:60:13
|
59 | def f9(
60 | arg: """Union[ # comment
| _____________^
61 | | float, # another
62 | | complex, int]"""
| |_____________________^
63 | ) -> "None":
64 | ...
|
help: Remove redundant type
57 |
58 |
59 | def f9(
- arg: """Union[ # comment
- float, # another
- complex, int]"""
60 + arg: """complex"""
61 | ) -> "None":
62 | ...
63 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `complex` instead of `int | float | complex`
--> PYI041_3.py:68:9
|
66 | def f10(
67 | arg: """
68 | / int | # comment
69 | | float | # another
70 | | complex
| |_______________^
71 | """
72 | ) -> "None":
|
help: Remove redundant type
65 |
66 | def f10(
67 | arg: """
- int | # comment
- float | # another
68 | complex
69 | """
70 | ) -> "None":
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `complex` instead of `int | float | complex`
--> PYI041_3.py:80:25
|
78 | ...
79 |
80 | def bad(self, arg: "int | float | complex") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^
81 | ...
|
help: Remove redundant type
77 | def good(self, arg: "int") -> "None":
78 | ...
79 |
- def bad(self, arg: "int | float | complex") -> "None":
80 + def bad(self, arg: "complex") -> "None":
81 | ...
82 |
83 | def bad2(self, arg: "int | Union[float, complex]") -> "None":
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `complex` instead of `int | float | complex`
--> PYI041_3.py:83:26
|
81 | ...
82 |
83 | def bad2(self, arg: "int | Union[float, complex]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
84 | ...
|
help: Remove redundant type
80 | def bad(self, arg: "int | float | complex") -> "None":
81 | ...
82 |
- def bad2(self, arg: "int | Union[float, complex]") -> "None":
83 + def bad2(self, arg: "complex") -> "None":
84 | ...
85 |
86 | def bad3(self, arg: "Union[Union[float, complex], int]") -> "None":
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `complex` instead of `int | float | complex`
--> PYI041_3.py:86:26
|
84 | ...
85 |
86 | def bad3(self, arg: "Union[Union[float, complex], int]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
87 | ...
|
help: Remove redundant type
83 | def bad2(self, arg: "int | Union[float, complex]") -> "None":
84 | ...
85 |
- def bad3(self, arg: "Union[Union[float, complex], int]") -> "None":
86 + def bad3(self, arg: "complex") -> "None":
87 | ...
88 |
89 | def bad4(self, arg: "Union[float | complex, int]") -> "None":
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `complex` instead of `int | float | complex`
--> PYI041_3.py:89:26
|
87 | ...
88 |
89 | def bad4(self, arg: "Union[float | complex, int]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
90 | ...
|
help: Remove redundant type
86 | def bad3(self, arg: "Union[Union[float, complex], int]") -> "None":
87 | ...
88 |
- def bad4(self, arg: "Union[float | complex, int]") -> "None":
89 + def bad4(self, arg: "complex") -> "None":
90 | ...
91 |
92 | def bad5(self, arg: "int | (float | complex)") -> "None":
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `complex` instead of `int | float | complex`
--> PYI041_3.py:92:26
|
90 | ...
91 |
92 | def bad5(self, arg: "int | (float | complex)") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^
93 | ...
|
help: Remove redundant type
89 | def bad4(self, arg: "Union[float | complex, int]") -> "None":
90 | ...
91 |
- def bad5(self, arg: "int | (float | complex)") -> "None":
92 + def bad5(self, arg: "complex") -> "None":
93 | ...
94 |
95 |
note: This is an unsafe fix and may change runtime behavior
PYI041 Use `float` instead of `int | float`
--> PYI041_3.py:99:24
|
97 | # fix must not yield runtime `None | None | ...` (TypeError)
98 | class Issue18298:
99 | def f1(self, arg: "None | int | None | float" = None) -> "None": # PYI041 - no fix
| ^^^^^^^^^^^^^^^^^^^^^^^^^
100 | pass
|
help: Remove redundant type
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_3.py:104:28
|
102 | if TYPE_CHECKING:
103 |
104 | def f2(self, arg: "None | int | None | float" = None) -> "None": ... # PYI041 - with fix
| ^^^^^^^^^^^^^^^^^^^^^^^^^
105 |
106 | else:
|
help: Remove redundant type
101 |
102 | if TYPE_CHECKING:
103 |
- def f2(self, arg: "None | int | None | float" = None) -> "None": ... # PYI041 - with fix
104 + def f2(self, arg: "None | None | float" = None) -> "None": ... # PYI041 - with fix
105 |
106 | else:
107 |
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_3.py:111:24
|
109 | pass
110 |
111 | def f3(self, arg: "None | float | None | int | None" = None) -> "None": # PYI041 - with fix
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
112 | pass
|
help: Remove redundant type
108 | def f2(self, arg=None) -> "None":
109 | pass
110 |
- 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 | pass
113 |
114 |
note: This is an unsafe fix and may change runtime behavior
PYI041 Use `complex` instead of `int | float | complex`
--> PYI041_3.py:119:24
|
117 | ...
118 |
119 | def bad(self, arg: "int " "| float | com" "plex") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
120 | ...
|
help: Remove redundant type
PYI041 Use `complex` instead of `int | float | complex`
--> PYI041_3.py:122:25
|
120 | ...
121 |
122 | def bad2(self, arg: "int | Union[flo" "at, complex]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
123 | ...
|
help: Remove redundant type
PYI041 Use `complex` instead of `int | float | complex`
--> PYI041_3.py:125:25
|
123 | ...
124 |
125 | def bad3(self, arg: "Union[Union[float, com" "plex], int]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
126 | ...
|
help: Remove redundant type
PYI041 Use `complex` instead of `int | float | complex`
--> PYI041_3.py:128:25
|
126 | ...
127 |
128 | def bad4(self, arg: "Union[float | complex, in" "t ]") -> "None":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
129 | ...
|
help: Remove redundant type
PYI041 Use `complex` instead of `int | float | complex`
--> PYI041_3.py:131:25
|
129 | ...
130 |
131 | def bad5(self, arg: "int | "
| _________________________^
132 | | "(float | complex)") -> "None":
| |___________________________________________^
133 | ...
|
help: Remove redundant type
PYI041 Use `complex` instead of `int | float | complex`
--> PYI041_3.py:135:25
|
133 | ...
134 |
135 | def bad6(self, arg: "in\
| _________________________^
136 | | t | (float | compl" "ex)") -> "None":
| |_________________________^
137 | ...
|
help: Remove redundant type

View file

@ -0,0 +1,71 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI041 Use `float` instead of `int | float`
--> PYI041_4.py:4:11
|
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": ...
|
help: Remove redundant type
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_4.py:5:12
|
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": ...
7 | def f4(a: "Uno[in\
|
help: Remove redundant type
2 |
3 |
4 | def f1(a: "U" "no[int, fl" "oat, Foo]") -> "None": ...
- def f2(a: "Uno[int, float, Foo]") -> "None": ...
5 + def f2(a: "Uno[float, Foo]") -> "None": ...
6 | def f3(a: """Uno[int, float, Foo]""") -> "None": ...
7 | def f4(a: "Uno[in\
8 | t, float, Foo]") -> "None": ...
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_4.py:6:14
|
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": ...
| ^^^^^^^^^^^^^^^^^^^^
7 | def f4(a: "Uno[in\
8 | t, float, Foo]") -> "None": ...
|
help: Remove redundant type
3 |
4 | def f1(a: "U" "no[int, fl" "oat, Foo]") -> "None": ...
5 | def f2(a: "Uno[int, float, Foo]") -> "None": ...
- def f3(a: """Uno[int, float, Foo]""") -> "None": ...
6 + def f3(a: """Uno[float, Foo]""") -> "None": ...
7 | def f4(a: "Uno[in\
8 | t, float, Foo]") -> "None": ...
note: This is an unsafe fix and may change runtime behavior
PYI041 [*] Use `float` instead of `int | float`
--> PYI041_4.py:7:11
|
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": ...
| |_______________^
|
help: Remove redundant type
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": ...
- def f4(a: "Uno[in\
- t, float, Foo]") -> "None": ...
7 + def f4(a: Uno[float, Foo]) -> "None": ...
note: This is an unsafe fix and may change runtime behavior