From 1baf98aab3e07355c62390668865c54be6258f5a Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 31 Oct 2025 10:50:54 -0400 Subject: [PATCH] [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. --- .../type_properties/is_disjoint_from.md | 25 +++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 11 +++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index d80a2b5b82..dfad076726 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -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 diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 4f8ee4c1fc..c3ff51e47f 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -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.