mirror of https://github.com/astral-sh/ruff
[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:
parent
f7528bd325
commit
5dc0079e78
|
|
@ -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]))
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue