[ty] Fix is_disjoint_from with @final classes (#21167)

## Summary

We currently perform a subtyping check instead of the intended subclass
check (and the subtyping check is confusingly named `is_subclass_of`).
This showed up in https://github.com/astral-sh/ruff/pull/21070.
This commit is contained in:
Ibraheem Ahmed 2025-10-31 10:50:54 -04:00 committed by GitHub
parent 3179b05221
commit 1baf98aab3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 33 additions and 3 deletions

View file

@ -87,6 +87,31 @@ static_assert(is_disjoint_from(memoryview, Foo))
static_assert(is_disjoint_from(type[memoryview], type[Foo]))
```
## Specialized `@final` types
```toml
[environment]
python-version = "3.12"
```
```py
from typing import final
from ty_extensions import static_assert, is_disjoint_from
@final
class Foo[T]:
def get(self) -> T:
raise NotImplementedError
class A: ...
class B: ...
static_assert(not is_disjoint_from(Foo[A], Foo[B]))
# TODO: `int` and `str` are disjoint bases, so these should be disjoint.
static_assert(not is_disjoint_from(Foo[int], Foo[str]))
```
## "Disjoint base" builtin types
Most other builtins can be subclassed and can even be used in multiple inheritance. However, builtin

View file

@ -637,12 +637,17 @@ impl<'db> ClassType<'db> {
return true;
}
// Optimisation: if either class is `@final`, we only need to do one `is_subclass_of` call.
if self.is_final(db) {
return self.is_subclass_of(db, other);
return self
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|class| class.class_literal(db).0 == other.class_literal(db).0);
}
if other.is_final(db) {
return other.is_subclass_of(db, self);
return other
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|class| class.class_literal(db).0 == self.class_literal(db).0);
}
// Two disjoint bases can only coexist in an MRO if one is a subclass of the other.