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 166d67edd0..02f50cc412 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 @@ -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])) ``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a5122431dd..492ed63f21 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -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. diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index ade46d77ca..58ccd28ca0 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -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( diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 9b046cd4cf..f0cc3bedc8 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -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,