diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index 7d1686fb2d..d4cc2bd3ec 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -141,3 +141,18 @@ class C: # revealed: (*, kw_only=Unknown | ((*, kw_only=Unknown) -> Unknown)) -> Unknown reveal_type(self.d) ``` + +## Self-referential implicit attributes + +```py +class Cyclic: + def __init__(self, data: str | dict): + self.data = data + + def update(self): + if isinstance(self.data, str): + self.data = {"url": self.data} + +# revealed: Unknown | str | dict[Unknown, Unknown] | dict[Unknown | str, Unknown | str] +reveal_type(Cyclic("").data) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index bdf406b060..ca06e8ca79 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -922,7 +922,30 @@ impl<'db> Type<'db> { { self } - _ => UnionType::from_elements_cycle_recovery(db, [self, previous]), + _ => { + // Also avoid unioning in a previous type which contains a Divergent from the + // current cycle, if the most-recent type does not. This cannot cause an + // oscillation, since Divergent is only introduced at the start of fixpoint + // iteration. + let has_divergent_type_in_cycle = |ty| { + any_over_type( + db, + ty, + &|nested_ty| { + matches!( + nested_ty, + Type::Dynamic(DynamicType::Divergent(DivergentType { id })) + if cycle.head_ids().contains(&id)) + }, + false, + ) + }; + if has_divergent_type_in_cycle(previous) && !has_divergent_type_in_cycle(self) { + self + } else { + UnionType::from_elements_cycle_recovery(db, [self, previous]) + } + } } .recursive_type_normalized(db, cycle) }