[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])) 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 ## "Disjoint base" builtin types
Most other builtins can be subclassed and can even be used in multiple inheritance. However, builtin 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; return true;
} }
// Optimisation: if either class is `@final`, we only need to do one `is_subclass_of` call.
if self.is_final(db) { 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) { 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. // Two disjoint bases can only coexist in an MRO if one is a subclass of the other.