Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[flake8-annotations] Fix parsing of "complex" forward reference type annotations in any-type (ANN401) #14700

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,38 @@ def __init__(self: "Foo", foo: int):
class Class:
def __init__(self):
print(f"{self.attr=}")



# Regression test for: https://github.com/astral-sh/ruff/issues/14695
# Checked for being valid Python using `typing.get_type_hints`

def f2(x: "'int'"):
pass

def f(x: "'in\x74'"):
pass

def f3(x: "'HelloWorld'"):
pass

def g(x: "'An\x79'"):
pass

def g2(x: "'Any'"):
pass

def g3(x: "An\x79"):
pass

def g4(x: "Any"):
pass

def h(x: "'Non\x65'"):
pass

def f4(x: "'in''\x74'"):
pass

def f5(x: "'An''\x79'"):
pass
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,248 @@ annotation_presence.py:165:9: ANN204 [*] Missing return type annotation for spec
165 |- def __init__(self):
165 |+ def __init__(self) -> None:
166 166 | print(f"{self.attr=}")
167 167 |
168 168 |

annotation_presence.py:173:5: ANN201 [*] Missing return type annotation for public function `f2`
|
171 | # Checked for being valid Python using `typing.get_type_hints`
172 |
173 | def f2(x: "'int'"):
| ^^ ANN201
174 | pass
|
= help: Add return type annotation: `None`

ℹ Unsafe fix
170 170 | # Regression test for: https://github.com/astral-sh/ruff/issues/14695
171 171 | # Checked for being valid Python using `typing.get_type_hints`
172 172 |
173 |-def f2(x: "'int'"):
173 |+def f2(x: "'int'") -> None:
174 174 | pass
175 175 |
176 176 | def f(x: "'in\x74'"):

annotation_presence.py:176:5: ANN201 [*] Missing return type annotation for public function `f`
|
174 | pass
175 |
176 | def f(x: "'in\x74'"):
| ^ ANN201
177 | pass
|
= help: Add return type annotation: `None`

ℹ Unsafe fix
173 173 | def f2(x: "'int'"):
174 174 | pass
175 175 |
176 |-def f(x: "'in\x74'"):
176 |+def f(x: "'in\x74'") -> None:
177 177 | pass
178 178 |
179 179 | def f3(x: "'HelloWorld'"):

annotation_presence.py:179:5: ANN201 [*] Missing return type annotation for public function `f3`
|
177 | pass
178 |
179 | def f3(x: "'HelloWorld'"):
| ^^ ANN201
180 | pass
|
= help: Add return type annotation: `None`

ℹ Unsafe fix
176 176 | def f(x: "'in\x74'"):
177 177 | pass
178 178 |
179 |-def f3(x: "'HelloWorld'"):
179 |+def f3(x: "'HelloWorld'") -> None:
180 180 | pass
181 181 |
182 182 | def g(x: "'An\x79'"):

annotation_presence.py:182:5: ANN201 [*] Missing return type annotation for public function `g`
|
180 | pass
181 |
182 | def g(x: "'An\x79'"):
| ^ ANN201
183 | pass
|
= help: Add return type annotation: `None`

ℹ Unsafe fix
179 179 | def f3(x: "'HelloWorld'"):
180 180 | pass
181 181 |
182 |-def g(x: "'An\x79'"):
182 |+def g(x: "'An\x79'") -> None:
183 183 | pass
184 184 |
185 185 | def g2(x: "'Any'"):

annotation_presence.py:182:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `x`
|
180 | pass
181 |
182 | def g(x: "'An\x79'"):
| ^^^^^^^^^^ ANN401
183 | pass
|

annotation_presence.py:185:5: ANN201 [*] Missing return type annotation for public function `g2`
|
183 | pass
184 |
185 | def g2(x: "'Any'"):
| ^^ ANN201
186 | pass
|
= help: Add return type annotation: `None`

ℹ Unsafe fix
182 182 | def g(x: "'An\x79'"):
183 183 | pass
184 184 |
185 |-def g2(x: "'Any'"):
185 |+def g2(x: "'Any'") -> None:
186 186 | pass
187 187 |
188 188 | def g3(x: "An\x79"):

annotation_presence.py:185:11: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `x`
|
183 | pass
184 |
185 | def g2(x: "'Any'"):
| ^^^^^^^ ANN401
186 | pass
|

annotation_presence.py:188:5: ANN201 [*] Missing return type annotation for public function `g3`
|
186 | pass
187 |
188 | def g3(x: "An\x79"):
| ^^ ANN201
189 | pass
|
= help: Add return type annotation: `None`

ℹ Unsafe fix
185 185 | def g2(x: "'Any'"):
186 186 | pass
187 187 |
188 |-def g3(x: "An\x79"):
188 |+def g3(x: "An\x79") -> None:
189 189 | pass
190 190 |
191 191 | def g4(x: "Any"):

annotation_presence.py:188:11: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `x`
|
186 | pass
187 |
188 | def g3(x: "An\x79"):
| ^^^^^^^^ ANN401
189 | pass
|

annotation_presence.py:191:5: ANN201 [*] Missing return type annotation for public function `g4`
|
189 | pass
190 |
191 | def g4(x: "Any"):
| ^^ ANN201
192 | pass
|
= help: Add return type annotation: `None`

ℹ Unsafe fix
188 188 | def g3(x: "An\x79"):
189 189 | pass
190 190 |
191 |-def g4(x: "Any"):
191 |+def g4(x: "Any") -> None:
192 192 | pass
193 193 |
194 194 | def h(x: "'Non\x65'"):

annotation_presence.py:191:11: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `x`
|
189 | pass
190 |
191 | def g4(x: "Any"):
| ^^^^^ ANN401
192 | pass
|

annotation_presence.py:194:5: ANN201 [*] Missing return type annotation for public function `h`
|
192 | pass
193 |
194 | def h(x: "'Non\x65'"):
| ^ ANN201
195 | pass
|
= help: Add return type annotation: `None`

ℹ Unsafe fix
191 191 | def g4(x: "Any"):
192 192 | pass
193 193 |
194 |-def h(x: "'Non\x65'"):
194 |+def h(x: "'Non\x65'") -> None:
195 195 | pass
196 196 |
197 197 | def f4(x: "'in''\x74'"):

annotation_presence.py:197:5: ANN201 [*] Missing return type annotation for public function `f4`
|
195 | pass
196 |
197 | def f4(x: "'in''\x74'"):
| ^^ ANN201
198 | pass
|
= help: Add return type annotation: `None`

ℹ Unsafe fix
194 194 | def h(x: "'Non\x65'"):
195 195 | pass
196 196 |
197 |-def f4(x: "'in''\x74'"):
197 |+def f4(x: "'in''\x74'") -> None:
198 198 | pass
199 199 |
200 200 | def f5(x: "'An''\x79'"):

annotation_presence.py:200:5: ANN201 [*] Missing return type annotation for public function `f5`
|
198 | pass
199 |
200 | def f5(x: "'An''\x79'"):
| ^^ ANN201
201 | pass
|
= help: Add return type annotation: `None`

ℹ Unsafe fix
197 197 | def f4(x: "'in''\x74'"):
198 198 | pass
199 199 |
200 |-def f5(x: "'An''\x79'"):
200 |+def f5(x: "'An''\x79'") -> None:
201 201 | pass

annotation_presence.py:200:11: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `x`
|
198 | pass
199 |
200 | def f5(x: "'An''\x79'"):
| ^^^^^^^^^^^^ ANN401
201 | pass
|
12 changes: 11 additions & 1 deletion crates/ruff_python_parser/src/typing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,17 @@ fn parse_simple_type_annotation(

fn parse_complex_type_annotation(string_expr: &ExprStringLiteral) -> AnnotationParseResult {
let mut parsed = parse_expression(string_expr.value.to_str())?;
relocate_expr(parsed.expr_mut(), string_expr.range());
// Remove quotes from nested forward reference type annotations
let range_excluding_quotes =
if let Expr::StringLiteral(ExprStringLiteral { ref value, .. }) = *parsed.syntax.body {
let string_parts = value.as_slice();
let start = string_parts[0].flags.opener_len();
let end = string_parts[string_parts.len() - 1].flags.closer_len();
string_expr.range().add_start(start).sub_end(end)
} else {
string_expr.range()
};
relocate_expr(parsed.expr_mut(), range_excluding_quotes);
Comment on lines -96 to +106
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way it would work with simple type annotations is that it first parses the outer string and then the inner string on the next call:

parse_simple_type_annotation: "'int'" (10..15)
parse_simple_type_annotation: "int" (11..14)

In the case of complex annotations, it seems like the range is always going to be the one without the inner quotes:

parse_complex_type_annotation: "'int'" (10..18)
parse_complex_type_annotation: "int" (10..18)

Ok(ParsedAnnotation {
parsed,
kind: AnnotationKind::Complex,
Expand Down