Skip to content

Commit 97acaae

Browse files
[ty] Fix stack overflow for self-referential TypeOf in annotations (#23407)
## Summary When a function references itself via `TypeOf` in a deferred annotation (`def foo(x: "TypeOf[foo]")`), the resulting `FunctionType` contains itself as a parameter type, forming a cycle. Closes astral-sh/ty#2800.
1 parent 1f380c8 commit 97acaae

3 files changed

Lines changed: 53 additions & 21 deletions

File tree

crates/ty_python_semantic/resources/mdtest/ty_extensions.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,18 @@ def f(x: TypeOf) -> None:
427427
reveal_type(x) # revealed: Unknown
428428
```
429429

430+
## Self-referential `TypeOf` in annotations
431+
432+
A function can reference itself via `TypeOf` in a deferred annotation. This should not cause a stack
433+
overflow:
434+
435+
```py
436+
from ty_extensions import TypeOf
437+
438+
def foo(x: "TypeOf[foo]"):
439+
reveal_type(x) # revealed: def foo(x: def foo(...)) -> Unknown
440+
```
441+
430442
## `CallableTypeOf`
431443

432444
The `CallableTypeOf` special form can be used to extract the `Callable` structural type inhabited by

crates/ty_python_semantic/src/types.rs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6408,25 +6408,27 @@ impl<'db> Type<'db> {
64086408
},
64096409
}
64106410

6411-
Type::FunctionLiteral(function) => match type_mapping {
6412-
// Promote the types within the signature before promoting the signature to its
6413-
// callable form.
6414-
TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => {
6415-
Type::FunctionLiteral(function.apply_type_mapping_impl(
6411+
Type::FunctionLiteral(function) => visitor.visit(self, || {
6412+
match type_mapping {
6413+
// Promote the types within the signature before promoting the signature to its
6414+
// callable form.
6415+
TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => {
6416+
Type::FunctionLiteral(function.apply_type_mapping_impl(
6417+
db,
6418+
type_mapping,
6419+
tcx,
6420+
visitor,
6421+
))
6422+
.promote_literals_impl(db)
6423+
}
6424+
_ => Type::FunctionLiteral(function.apply_type_mapping_impl(
64166425
db,
64176426
type_mapping,
64186427
tcx,
64196428
visitor,
6420-
))
6421-
.promote_literals_impl(db)
6429+
)),
64226430
}
6423-
_ => Type::FunctionLiteral(function.apply_type_mapping_impl(
6424-
db,
6425-
type_mapping,
6426-
tcx,
6427-
visitor,
6428-
)),
6429-
},
6431+
}),
64306432

64316433
Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new(
64326434
db,
@@ -6692,7 +6694,9 @@ impl<'db> Type<'db> {
66926694
}
66936695

66946696
Type::FunctionLiteral(function) => {
6695-
function.find_legacy_typevars_impl(db, binding_context, typevars, visitor);
6697+
visitor.visit(self, || {
6698+
function.find_legacy_typevars_impl(db, binding_context, typevars, visitor);
6699+
});
66966700
}
66976701

66986702
Type::BoundMethod(method) => {

crates/ty_python_semantic/src/types/display.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ pub struct DisplaySettings<'db> {
8787
/// whose type parameters are currently being displayed).
8888
/// Used to suppress redundant `@{scope}` suffixes for type variables.
8989
pub active_scopes: Rc<FxHashSet<Definition<'db>>>,
90+
/// Function types that are currently being displayed.
91+
/// Used to prevent infinite recursion when displaying self-referential function types.
92+
pub visited_function_types: Rc<FxHashSet<FunctionType<'db>>>,
9093
}
9194

9295
impl<'db> DisplaySettings<'db> {
@@ -1466,6 +1469,19 @@ pub(crate) struct DisplayFunctionType<'db> {
14661469

14671470
impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> {
14681471
fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
1472+
// Detect self-referential function types to prevent infinite recursion.
1473+
if self.settings.visited_function_types.contains(&self.ty) {
1474+
f.set_invalid_type_annotation();
1475+
f.write_str("def ")?;
1476+
write!(f, "{}", self.ty.name(self.db))?;
1477+
return f.write_str("(...)");
1478+
}
1479+
1480+
let mut settings = self.settings.clone();
1481+
let mut visited = (*settings.visited_function_types).clone();
1482+
visited.insert(self.ty);
1483+
settings.visited_function_types = Rc::new(visited);
1484+
14691485
let signature = self.ty.signature(self.db);
14701486

14711487
match signature.overloads.as_slice() {
@@ -1475,32 +1491,32 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> {
14751491
let type_parameters = DisplayOptionalGenericContext {
14761492
generic_context: signature.generic_context.as_ref(),
14771493
db: self.db,
1478-
settings: self.settings.clone(),
1494+
settings: settings.clone(),
14791495
hide_unused_self,
14801496
};
14811497
f.set_invalid_type_annotation();
14821498
f.write_str("def ")?;
14831499
write!(f, "{}", self.ty.name(self.db))?;
14841500
type_parameters.fmt_detailed(f)?;
14851501
signature
1486-
.display_with(self.db, self.settings.disallow_signature_name())
1502+
.display_with(self.db, settings.disallow_signature_name())
14871503
.fmt_detailed(f)
14881504
}
14891505
signatures => {
14901506
// TODO: How to display overloads?
1491-
if !self.settings.multiline {
1507+
if !settings.multiline {
14921508
// TODO: This should ideally have a TypeDetail but we actually
14931509
// don't have a type for @overload (we just detect the decorator)
14941510
f.write_str("Overload")?;
14951511
f.write_char('[')?;
14961512
}
1497-
let separator = if self.settings.multiline { "\n" } else { ", " };
1513+
let separator = if settings.multiline { "\n" } else { ", " };
14981514
let mut join = f.join(separator);
14991515
for signature in signatures {
1500-
join.entry(&signature.display_with(self.db, self.settings.clone()));
1516+
join.entry(&signature.display_with(self.db, settings.clone()));
15011517
}
15021518
join.finish()?;
1503-
if !self.settings.multiline {
1519+
if !settings.multiline {
15041520
f.write_str("]")?;
15051521
}
15061522
Ok(())

0 commit comments

Comments
 (0)