diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_annotations/annotation_presence.py b/crates/ruff_linter/resources/test/fixtures/flake8_annotations/annotation_presence.py index 4ef4e2220bb93..edad5b2d8b067 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_annotations/annotation_presence.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_annotations/annotation_presence.py @@ -164,3 +164,9 @@ 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 +# A quoted annotation containing a nested string with an escape sequence +# should not cause a stack overflow. +def quoted_escape(x: "'in\x74'"): pass diff --git a/crates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__defaults.snap b/crates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__defaults.snap index 80b35c0a76c2a..7a7220477d0d7 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__defaults.snap +++ b/crates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__defaults.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs +assertion_line: 36 --- ANN201 [*] Missing return type annotation for public function `foo` --> annotation_presence.py:5:5 @@ -319,4 +320,31 @@ help: Add return type annotation: `None` - def __init__(self): 165 + def __init__(self) -> None: 166 | print(f"{self.attr=}") +167 | +168 | note: This is an unsafe fix and may change runtime behavior + +ANN201 [*] Missing return type annotation for public function `quoted_escape` + --> annotation_presence.py:172:5 + | +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 + | ^^^^^^^^^^^^^ + | +help: Add return type annotation: `None` +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. + - def quoted_escape(x: "'in\x74'"): pass +172 + def quoted_escape(x: "'in\x74'") -> None: pass +note: This is an unsafe fix and may change runtime behavior + +ANN401 Dynamically typed expressions (typing.Any) are disallowed in `x` + --> annotation_presence.py:172:22 + | +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 + | ^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/rules/ruff/typing.rs b/crates/ruff_linter/src/rules/ruff/typing.rs index 3c650bfe0bb69..0c1b355860d7e 100644 --- a/crates/ruff_linter/src/rules/ruff/typing.rs +++ b/crates/ruff_linter/src/rules/ruff/typing.rs @@ -114,8 +114,17 @@ impl<'a> TypingTarget<'a> { Expr::StringLiteral(string_expr) => checker .parse_type_annotation(string_expr) .ok() - .map(|parsed_annotation| { - TypingTarget::ForwardReference(parsed_annotation.expression()) + .and_then(|parsed_annotation| { + let inner = parsed_annotation.expression(); + if inner.is_string_literal_expr() { + // A forward reference that resolves to another string literal + // (e.g. `"'int'"`) is not a valid type annotation. Avoid + // recursing, which would loop forever when `relocate_expr` + // gives the inner literal the same text range as the outer. + None + } else { + Some(TypingTarget::ForwardReference(inner)) + } }), _ => semantic.resolve_qualified_name(expr).map_or( // If we can't resolve the call path, it must be defined in the