Skip to content

Commit 3f94c6a

Browse files
authored
Fix stack overflow in ANN401 on quoted annotations with escape sequences (#23912)
1 parent 91fc7bd commit 3f94c6a

3 files changed

Lines changed: 45 additions & 2 deletions

File tree

crates/ruff_linter/resources/test/fixtures/flake8_annotations/annotation_presence.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,9 @@ def __init__(self: "Foo", foo: int):
164164
class Class:
165165
def __init__(self):
166166
print(f"{self.attr=}")
167+
168+
169+
# Regression test for: https://github.com/astral-sh/ruff/issues/14695
170+
# A quoted annotation containing a nested string with an escape sequence
171+
# should not cause a stack overflow.
172+
def quoted_escape(x: "'in\x74'"): pass

crates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__defaults.snap

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
3+
assertion_line: 36
34
---
45
ANN201 [*] Missing return type annotation for public function `foo`
56
--> annotation_presence.py:5:5
@@ -319,4 +320,31 @@ help: Add return type annotation: `None`
319320
- def __init__(self):
320321
165 + def __init__(self) -> None:
321322
166 | print(f"{self.attr=}")
323+
167 |
324+
168 |
322325
note: This is an unsafe fix and may change runtime behavior
326+
327+
ANN201 [*] Missing return type annotation for public function `quoted_escape`
328+
--> annotation_presence.py:172:5
329+
|
330+
170 | # A quoted annotation containing a nested string with an escape sequence
331+
171 | # should not cause a stack overflow.
332+
172 | def quoted_escape(x: "'in\x74'"): pass
333+
| ^^^^^^^^^^^^^
334+
|
335+
help: Add return type annotation: `None`
336+
169 | # Regression test for: https://github.com/astral-sh/ruff/issues/14695
337+
170 | # A quoted annotation containing a nested string with an escape sequence
338+
171 | # should not cause a stack overflow.
339+
- def quoted_escape(x: "'in\x74'"): pass
340+
172 + def quoted_escape(x: "'in\x74'") -> None: pass
341+
note: This is an unsafe fix and may change runtime behavior
342+
343+
ANN401 Dynamically typed expressions (typing.Any) are disallowed in `x`
344+
--> annotation_presence.py:172:22
345+
|
346+
170 | # A quoted annotation containing a nested string with an escape sequence
347+
171 | # should not cause a stack overflow.
348+
172 | def quoted_escape(x: "'in\x74'"): pass
349+
| ^^^^^^^^^^
350+
|

crates/ruff_linter/src/rules/ruff/typing.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,17 @@ impl<'a> TypingTarget<'a> {
114114
Expr::StringLiteral(string_expr) => checker
115115
.parse_type_annotation(string_expr)
116116
.ok()
117-
.map(|parsed_annotation| {
118-
TypingTarget::ForwardReference(parsed_annotation.expression())
117+
.and_then(|parsed_annotation| {
118+
let inner = parsed_annotation.expression();
119+
if inner.is_string_literal_expr() {
120+
// A forward reference that resolves to another string literal
121+
// (e.g. `"'int'"`) is not a valid type annotation. Avoid
122+
// recursing, which would loop forever when `relocate_expr`
123+
// gives the inner literal the same text range as the outer.
124+
None
125+
} else {
126+
Some(TypingTarget::ForwardReference(inner))
127+
}
119128
}),
120129
_ => semantic.resolve_qualified_name(expr).map_or(
121130
// If we can't resolve the call path, it must be defined in the

0 commit comments

Comments
 (0)