From 0138cd238a8669d53c325eb7ca19155946dfa665 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 11 Dec 2025 19:52:34 -0800 Subject: [PATCH] [ty] avoid fixpoint unioning of types containing current-cycle Divergent (#21910) Partially addresses https://github.com/astral-sh/ty/issues/1732 ## Summary Don't union the previous type in fixpoint iteration if the previous type contains a `Divergent` from the current cycle and the latest type does not. The theory here, as outlined by @mtshiba at https://github.com/astral-sh/ty/issues/1732#issuecomment-3609937420, is that oscillation can't occur by removing and then reintroducing a `Divergent` type repeatedly, since `Divergent` types are only introduced at the start of fixpoint iteration. ## Test Plan Removes a `Divergent` type from the added mdtest, doesn't otherwise regress any tests. --- .../resources/mdtest/cycle.md | 15 +++++++++++ crates/ty_python_semantic/src/types.rs | 25 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) 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) }