[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.
This commit is contained in:
Carl Meyer 2025-12-11 09:53:43 -08:00 committed by GitHub
parent c548ef2027
commit 4fdb4e8219
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 13 additions and 7 deletions

View File

@ -860,9 +860,6 @@ reveal_type(Sub) # revealed: <class 'Sub'>
U = TypeVar("U") U = TypeVar("U")
class Base2(Generic[T, U]): ... class Base2(Generic[T, U]): ...
# TODO: no error
# error: [unsupported-base] "Unsupported class base with type `<class 'Base2[Sub2, U@Sub2]'> | <class 'Base2[Sub2[Unknown], U@Sub2]'>`"
class Sub2(Base2["Sub2", U]): ... class Sub2(Base2["Sub2", U]): ...
``` ```
@ -888,8 +885,6 @@ from typing_extensions import Generic, TypeVar
T = TypeVar("T") T = TypeVar("T")
# TODO: no error "Unsupported class base with type `<class 'list[Derived[T@Derived]]'> | <class 'list[@Todo]'>`"
# error: [unsupported-base]
class Derived(list[Derived[T]], Generic[T]): ... class Derived(list[Derived[T]], Generic[T]): ...
``` ```

View File

@ -912,8 +912,19 @@ impl<'db> Type<'db> {
previous: Self, previous: Self,
cycle: &salsa::Cycle, cycle: &salsa::Cycle,
) -> Self { ) -> Self {
UnionType::from_elements_cycle_recovery(db, [self, previous]) // Avoid unioning two generic aliases of the same class together; this union will never
.recursive_type_normalized(db, cycle) // 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 { fn is_none(&self, db: &'db dyn Db) -> bool {