From 4fdb4e8219bf8e7b8001121dd61a7c3678200bbf Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 11 Dec 2025 09:53:43 -0800 Subject: [PATCH] [ty] avoid unions of generic aliases of the same class in fixpoint (#21909) Partially addresses https://github.com/astral-sh/ty/issues/1732 Fixes https://github.com/astral-sh/ty/issues/1800 ## Summary At each fixpoint iteration, we union the "previous" and "current" iteration types, to ensure that the type can only widen at each iteration. This prevents oscillation and ensures convergence. But some unions triggered by this behavior (in particular, unions of differently-specialized generic-aliases of the same class) never simplify, and cause spurious errors. Since we haven't seen examples of oscillating types involving class-literal or generic-alias types, just don't union those. There may be more thorough/principled ways to avoid undesirable unions in fixpoint iteration, but this narrow change seems like it results in strict improvement. ## Test Plan Removes two false positive `unsupported-class-base` in mdtests, and several in the ecosystem, without causing other regression. --- .../resources/mdtest/generics/legacy/classes.md | 5 ----- crates/ty_python_semantic/src/types.rs | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index bc6ccdb7c1..30d6a89ec0 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -860,9 +860,6 @@ reveal_type(Sub) # revealed: U = TypeVar("U") class Base2(Generic[T, U]): ... - -# TODO: no error -# error: [unsupported-base] "Unsupported class base with type ` | `" class Sub2(Base2["Sub2", U]): ... ``` @@ -888,8 +885,6 @@ from typing_extensions import Generic, TypeVar T = TypeVar("T") -# TODO: no error "Unsupported class base with type ` | `" -# error: [unsupported-base] class Derived(list[Derived[T]], Generic[T]): ... ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 54beaf3037..e2050ec45f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -912,8 +912,19 @@ impl<'db> Type<'db> { previous: Self, cycle: &salsa::Cycle, ) -> Self { - UnionType::from_elements_cycle_recovery(db, [self, previous]) - .recursive_type_normalized(db, cycle) + // Avoid unioning two generic aliases of the same class together; this union will never + // simplify and is likely to cause downstream problems. This introduces the theoretical + // possibility of cycle oscillation involving such types (because we are not strictly + // widening the type on each iteration), but so far we have not seen an example of that. + match (previous, self) { + (Type::GenericAlias(prev_alias), Type::GenericAlias(curr_alias)) + if prev_alias.origin(db) == curr_alias.origin(db) => + { + self + } + _ => UnionType::from_elements_cycle_recovery(db, [self, previous]), + } + .recursive_type_normalized(db, cycle) } fn is_none(&self, db: &'db dyn Db) -> bool {