[ty] Fix disjointness checks on `@final` class instances (#21769)

## Summary

This was left unfinished in
https://github.com/astral-sh/ruff/pull/21167. This is required to fix
our disjointness checks with type-of a final class, which is currently
broken, and blocking https://github.com/astral-sh/ty/issues/159.
This commit is contained in:
Ibraheem Ahmed 2025-12-10 14:17:22 -05:00 committed by GitHub
parent f7528bd325
commit 5dc0079e78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 132 additions and 21 deletions

View File

@ -95,7 +95,7 @@ python-version = "3.12"
```
```py
from typing import final
from typing import Any, final
from ty_extensions import static_assert, is_disjoint_from
@final
@ -106,9 +106,12 @@ class Foo[T]:
class A: ...
class B: ...
static_assert(not is_disjoint_from(A, B))
static_assert(not is_disjoint_from(Foo[A], Foo[B]))
static_assert(not is_disjoint_from(Foo[A], Foo[Any]))
static_assert(not is_disjoint_from(Foo[Any], Foo[B]))
# TODO: `int` and `str` are disjoint bases, so these should be disjoint.
# `Foo[Never]` is a subtype of both `Foo[int]` and `Foo[str]`.
static_assert(not is_disjoint_from(Foo[int], Foo[str]))
```

View File

@ -718,6 +718,31 @@ impl<'db> ClassType<'db> {
.find_map(|base| base.as_disjoint_base(db))
}
/// Return `true` if this class could exist in the MRO of `other`.
pub(super) fn could_exist_in_mro_of(self, db: &'db dyn Db, other: Self) -> bool {
other
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|class| match (self, class) {
(ClassType::NonGeneric(this_class), ClassType::NonGeneric(other_class)) => {
this_class == other_class
}
(ClassType::Generic(this_alias), ClassType::Generic(other_alias)) => {
this_alias.origin(db) == other_alias.origin(db)
&& !this_alias
.specialization(db)
.is_disjoint_from(
db,
other_alias.specialization(db),
InferableTypeVars::None,
)
.is_always_satisfied(db)
}
(ClassType::NonGeneric(_), ClassType::Generic(_))
| (ClassType::Generic(_), ClassType::NonGeneric(_)) => false,
})
}
/// Return `true` if this class could coexist in an MRO with `other`.
///
/// For two given classes `A` and `B`, it is often possible to say for sure
@ -729,16 +754,11 @@ impl<'db> ClassType<'db> {
}
if self.is_final(db) {
return self
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|class| class.class_literal(db).0 == other.class_literal(db).0);
return other.could_exist_in_mro_of(db, self);
}
if other.is_final(db) {
return other
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|class| class.class_literal(db).0 == self.class_literal(db).0);
return self.could_exist_in_mro_of(db, other);
}
// Two disjoint bases can only coexist in an MRO if one is a subclass of the other.

View File

@ -11,7 +11,7 @@ use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKind, ScopeId};
use crate::semantic_index::{SemanticIndex, semantic_index};
use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::constraints::ConstraintSet;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::instance::{Protocol, ProtocolInstanceType};
use crate::types::signatures::{Parameters, ParametersKind};
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
@ -1226,18 +1226,20 @@ impl<'db> Specialization<'db> {
let self_materialization_kind = self.materialization_kind(db);
let other_materialization_kind = other.materialization_kind(db);
let mut result = ConstraintSet::from(true);
for ((bound_typevar, self_type), other_type) in (generic_context.variables(db))
.zip(self.types(db))
.zip(other.types(db))
{
let types = itertools::izip!(
generic_context.variables(db),
self.types(db),
other.types(db)
);
types.when_all(db, |(bound_typevar, self_type, other_type)| {
// Subtyping/assignability of each type in the specialization depends on the variance
// of the corresponding typevar:
// - covariant: verify that self_type <: other_type
// - contravariant: verify that other_type <: self_type
// - invariant: verify that self_type <: other_type AND other_type <: self_type
// - bivariant: skip, can't make subtyping/assignability false
let compatible = match bound_typevar.variance(db) {
match bound_typevar.variance(db) {
TypeVarVariance::Invariant => has_relation_in_invariant_position(
db,
self_type,
@ -1266,13 +1268,82 @@ impl<'db> Specialization<'db> {
disjointness_visitor,
),
TypeVarVariance::Bivariant => ConstraintSet::from(true),
};
if result.intersect(db, compatible).is_never_satisfied(db) {
return result;
}
})
}
pub(crate) fn is_disjoint_from(
self,
db: &'db dyn Db,
other: Self,
inferable: InferableTypeVars<'_, 'db>,
) -> ConstraintSet<'db> {
self.is_disjoint_from_impl(
db,
other,
inferable,
&IsDisjointVisitor::default(),
&HasRelationToVisitor::default(),
)
}
pub(crate) fn is_disjoint_from_impl(
self,
db: &'db dyn Db,
other: Self,
inferable: InferableTypeVars<'_, 'db>,
disjointness_visitor: &IsDisjointVisitor<'db>,
relation_visitor: &HasRelationToVisitor<'db>,
) -> ConstraintSet<'db> {
let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) {
return ConstraintSet::from(true);
}
result
if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db))
{
return self_tuple.is_disjoint_from_impl(
db,
other_tuple,
inferable,
disjointness_visitor,
relation_visitor,
);
}
let types = itertools::izip!(
generic_context.variables(db),
self.types(db),
other.types(db)
);
types.when_all(
db,
|(bound_typevar, self_type, other_type)| match bound_typevar.variance(db) {
// TODO: This check can lead to false negatives.
//
// For example, `Foo[int]` and `Foo[bool]` are disjoint, even though `bool` is a subtype
// of `int`. However, given two non-inferable type variables `T` and `U`, `Foo[T]` and
// `Foo[U]` should not be considered disjoint, as `T` and `U` could be specialized to the
// same type. We don't currently have a good typing relationship to represent this.
TypeVarVariance::Invariant => self_type.is_disjoint_from_impl(
db,
*other_type,
inferable,
disjointness_visitor,
relation_visitor,
),
// If `Foo[T]` is covariant in `T`, `Foo[Never]` is a subtype of `Foo[A]` and `Foo[B]`
TypeVarVariance::Covariant => ConstraintSet::from(false),
// If `Foo[T]` is contravariant in `T`, `Foo[A | B]` is a subtype of `Foo[A]` and `Foo[B]`
TypeVarVariance::Contravariant => ConstraintSet::from(false),
// If `Foo[T]` is bivariant in `T`, `Foo[A]` and `Foo[B]` are mutual subtypes.
TypeVarVariance::Bivariant => ConstraintSet::from(false),
},
)
}
pub(crate) fn is_equivalent_to_impl(

View File

@ -288,6 +288,23 @@ impl<'db> TupleType<'db> {
)
}
pub(crate) fn is_disjoint_from_impl(
self,
db: &'db dyn Db,
other: Self,
inferable: InferableTypeVars<'_, 'db>,
disjointness_visitor: &IsDisjointVisitor<'db>,
relation_visitor: &HasRelationToVisitor<'db>,
) -> ConstraintSet<'db> {
self.tuple(db).is_disjoint_from_impl(
db,
other.tuple(db),
inferable,
disjointness_visitor,
relation_visitor,
)
}
pub(crate) fn is_equivalent_to_impl(
self,
db: &'db dyn Db,