diff --git a/crates/ty_python_semantic/resources/mdtest/generics/set_theoretic.md b/crates/ty_python_semantic/resources/mdtest/generics/set_theoretic.md index 94ab8223f45554..5774d3bedd3c7a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/set_theoretic.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/set_theoretic.md @@ -7,9 +7,14 @@ This test suite explores the interplay between generics and set theoretic gradua python-version = "3.14" ``` +## Derivations and general results + +This section concentrates on deriving the main results while the next section covers some more edge +cases. + ```pyi from typing import Any -from ty_extensions import static_assert, is_equivalent_to, is_subtype_of +from ty_extensions import Bottom, static_assert, is_equivalent_to, is_subtype_of ``` Throughout the document, we use the following classes as canonical examples for covariant, @@ -192,12 +197,19 @@ Contra[P] & Contra[Any] = Contra[P | Any] (5b) We can encode all of these in ty assertions: ```pyi -# TODO: all of these should pass +# TODO: both should pass static_assert(is_equivalent_to(Co[P] | Co[Any], Co[P | Any])) # error: [static-assert-error] -static_assert(is_equivalent_to(Co[P] & Co[Any], Co[P & Any])) # error: [static-assert-error] +static_assert(is_equivalent_to(Co[Any] | Co[P], Co[P | Any])) # error: [static-assert-error] + +static_assert(is_equivalent_to(Co[P] & Co[Any], Co[P & Any])) +static_assert(is_equivalent_to(Co[Any] & Co[P], Co[P & Any])) +# TODO: both should pass static_assert(is_equivalent_to(Contra[P] | Contra[Any], Contra[P & Any])) # error: [static-assert-error] -static_assert(is_equivalent_to(Contra[P] & Contra[Any], Contra[P | Any])) # error: [static-assert-error] +static_assert(is_equivalent_to(Contra[Any] | Contra[P], Contra[P & Any])) # error: [static-assert-error] + +static_assert(is_equivalent_to(Contra[P] & Contra[Any], Contra[P | Any])) +static_assert(is_equivalent_to(Contra[Any] & Contra[P], Contra[P | Any])) ``` What about invariance? We can naively write `Invariant[Any]` in its interval representation: @@ -285,3 +297,143 @@ static_assert(is_equivalent_to(Invariant[P] & Invariant[Any], Invariant[P])) static_assert(not is_equivalent_to(Invariant[P] | Invariant[Any], Invariant[P])) ``` + +We can also consider intersections with the negation of an `Any`-specialized generic class. For all +of the generic classes `C` above, we can negate the canonical interval representation of `C[Any]`: + +```ignore +~C[Any] + = ~(Bottom[C[Any]] | Top[C[Any]] & Any) + = ~Bottom[C[Any]] & (~Top[C[Any]] | Any) + = ~Top[C[Any]] | ~Bottom[C[Any]] & Any +``` + +Every fully-static specialization `C[P]` is a subtype of `Top[C[Any]]`, therefore: + +```ignore +C[P] & ~C[Any] + = C[P] & (~Top[C[Any]] | ~Bottom[C[Any]] & Any) + = C[P] & ~Bottom[C[Any]] & Any +``` + +For covariant and contravariant classes, we can simplify the bottom materializations as in (3a) and +(3b): + +```ignore +Co[P] & ~Co[Any] = Co[P] & ~Co[Never] & Any +Contra[P] & ~Contra[Any] = Contra[P] & ~Contra[object] & Any +Invariant[P] & ~Invariant[Any] = Invariant[P] & ~Bottom[Invariant[Any]] & Any +``` + +We can verify all three relations in ty: + +```pyi +static_assert(is_equivalent_to(Co[P] & ~Co[Any], Co[P] & ~Bottom[Co[Any]] & Any)) +static_assert(is_equivalent_to(~Co[Any] & Co[P], Co[P] & ~Bottom[Co[Any]] & Any)) + +static_assert(is_equivalent_to(Contra[P] & ~Contra[Any], Contra[P] & ~Bottom[Contra[Any]] & Any)) +static_assert(is_equivalent_to(~Contra[Any] & Contra[P], Contra[P] & ~Bottom[Contra[Any]] & Any)) + +static_assert(is_equivalent_to(Invariant[P] & ~Invariant[Any], Invariant[P] & ~Bottom[Invariant[Any]] & Any)) +static_assert(is_equivalent_to(~Invariant[Any] & Invariant[P], Invariant[P] & ~Bottom[Invariant[Any]] & Any)) +``` + +## Edge cases + +### Multi-parameter and mixed-variance generics + +The results above naturally extend to multi-parameter generics and mixed variances: + +```pyi +from typing import Any, Generic, TypeVar +from ty_extensions import Bottom, static_assert, is_equivalent_to + +class P: ... +class Q: ... +class R: ... + +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) +T_invariant = TypeVar("T_invariant") + +class Mixed(Generic[T_co, T_contra, T_invariant]): ... + +static_assert(is_equivalent_to(Mixed[P, Q, R] & Mixed[Any, Any, Any], Mixed[P & Any, Q | Any, R])) +``` + +### Tuples + +Tuple types are covariant in every type parameter, so the results derived for `Co[T]` above apply to +`tuple` at every position: + +```pyi +from typing import Any, Never +from ty_extensions import static_assert, is_equivalent_to + +class P: ... +class Q: ... + +static_assert(is_equivalent_to(tuple[P] & tuple[Any], tuple[P & Any])) +static_assert(is_equivalent_to(tuple[Any] & tuple[P], tuple[P & Any])) + +static_assert(is_equivalent_to(tuple[P] & ~tuple[Any], tuple[P] & ~tuple[Never] & Any)) +static_assert(is_equivalent_to(~tuple[Any] & tuple[P], tuple[P] & ~tuple[Never] & Any)) + +static_assert(is_equivalent_to(tuple[P, Q] & tuple[Any, Q], tuple[P & Any, Q])) +static_assert(is_equivalent_to(tuple[Any, Q] & tuple[P, Q], tuple[P & Any, Q])) + +static_assert(is_equivalent_to(tuple[P, Q] & tuple[P, Any], tuple[P, Q & Any])) +static_assert(is_equivalent_to(tuple[P, Any] & tuple[P, Q], tuple[P, Q & Any])) + +static_assert(is_equivalent_to(tuple[P, Q] & tuple[Any, Any], tuple[P & Any, Q & Any])) +static_assert(is_equivalent_to(tuple[Any, Any] & tuple[P, Q], tuple[P & Any, Q & Any])) +``` + +Intersections with tuples of different length are not affected: + +```pyi +static_assert(is_equivalent_to(tuple[int, str] & ~tuple[Any], tuple[int, str])) +static_assert(is_equivalent_to(tuple[int, str] & ~tuple[int, str, Any], tuple[int, str])) +``` + +### `type[...]` + +`type[..]` can also be seen as a covariant type constructor, so the results also hold here: + +```pyi +from typing import Any, Never +from ty_extensions import static_assert, is_equivalent_to + +class P: ... +class Q: ... + +static_assert(is_equivalent_to(type[P] & type[Any], type[P & Any])) +static_assert(is_equivalent_to(type[Any] & type[P], type[P & Any])) + +static_assert(is_equivalent_to(type[P] & ~type[Any], type[P] & ~type[Never] & Any)) +static_assert(is_equivalent_to(~type[Any] & type[P], type[P] & ~type[Never] & Any)) +``` + +### Type var bounds and `NewTypes` + +The simplification preserve the identities of type variables and `NewType` instances: + +```pyi +from typing import Any, NewType +from ty_extensions import is_equivalent_to, static_assert + +class P: ... +class Q: ... + +class Co[T]: + def get(self) -> T: + raise NotImplementedError + +CoId = NewType("CoId", Co[P]) + +def preserve_typevar[T: Co[P]](value: T & Co[Any]) -> T: + return value + +def preserve_newtype(value: CoId & Co[Any]) -> CoId: + return value +``` diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index d110a0b75b3447..5cc612f39aa644 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -1455,7 +1455,7 @@ def expects_named_tuple(x: typing.NamedTuple): reveal_type(x.__iter__) def _(y: type[typing.NamedTuple]): - reveal_type(y) # revealed: @Todo(unsupported type[X] special form) + reveal_type(y) # revealed: type[tuple[object, ...]] & type[NamedTupleLike] # error: [invalid-type-form] "Special form `typing.NamedTuple` expected no type parameter" def _(z: typing.NamedTuple[int]): ... diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 63f0dd268fdf85..eaeaee68840d42 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1723,7 +1723,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } } -fn specialization_variance<'db>( +pub(super) fn specialization_variance<'db>( db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>, ) -> TypeVarVariance { diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index acf1007d650c74..48ae4ce955a7b7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1244,7 +1244,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::StringLiteral(_) => { infer_type_argument(self, slice) } - ast::Expr::BinOp(binary) if binary.op == ast::Operator::BitOr => { + ast::Expr::BinOp(binary) + if matches!(binary.op, ast::Operator::BitOr | ast::Operator::BitAnd) => + { infer_type_argument(self, slice) } ast::Expr::Tuple(_) => { diff --git a/crates/ty_python_semantic/src/types/set_theoretic.rs b/crates/ty_python_semantic/src/types/set_theoretic.rs index 10bcd594a0f9ae..701c859263f63b 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic.rs @@ -12,6 +12,7 @@ use crate::types::{TypeVarBoundOrConstraints, visitor}; use crate::{Db, FxOrderSet}; pub(crate) mod builder; +mod generic_gradual_intersections; pub(crate) use builder::{IntersectionBuilder, UnionBuilder}; diff --git a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs index 970cb694604617..21655f470442e9 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs @@ -37,12 +37,15 @@ //! (unless exactly the same literal type), we can avoid many unnecessary redundancy checks. use super::RecursivelyDefined; +use super::generic_gradual_intersections::{ + generic_gradual_intersection, negated_generalization_bottom, +}; use crate::types::enums::EnumComplement; use crate::types::set_theoretic::expand_intersection_typevars_and_newtypes; use crate::types::{ BytesLiteralType, ClassLiteral, EnumLiteralType, IntersectionType, KnownClass, KnownInstanceType, LiteralValueType, LiteralValueTypeKind, NegativeIntersectionElements, - StringLiteralType, SubclassOfType, Type, TypeVarBoundOrConstraints, TypeVarVariance, UnionType, + StringLiteralType, SubclassOfType, Type, TypeVarBoundOrConstraints, UnionType, }; use crate::{Db, FxOrderMap, FxOrderSet}; use rustc_hash::FxHashSet; @@ -90,63 +93,6 @@ fn split_truthiness_guarded_intersection<'db>( Some((core.build(), guard)) } -/// Return `true` if `general` and `specific` are specializations of the same generic class and -/// `general` only differs by using dynamic types for invariant type variables. For example, -/// `list[Any]` is an invariant-dynamic generalization of `list[int]`. -fn is_invariant_dynamic_generalization_of<'db>( - db: &'db dyn Db, - general: Type<'db>, - specific: Type<'db>, -) -> bool { - // Fast path to avoid performance regressions. - if !general.has_dynamic(db) { - return false; - } - - if matches!(general, Type::TypeVar(_) | Type::NewTypeInstance(_)) { - return false; - } - - let ( - Some((general_class, general_specialization)), - Some((specific_class, specific_specialization)), - ) = ( - general.class_specialization(db), - specific.class_specialization(db), - ) - else { - return false; - }; - - // Top and bottom materializations are not gradual types. - if general_class != specific_class - || general_specialization.materialization_kind(db).is_some() - || specific_specialization.materialization_kind(db).is_some() - { - return false; - } - - let mut has_dynamic_replacement = false; - for ((typevar, general_type), specific_type) in general_specialization - .generic_context(db) - .variables(db) - .zip(general_specialization.types(db)) - .zip(specific_specialization.types(db)) - { - if general_type == specific_type { - continue; - } - if general_type.is_non_divergent_dynamic() - && typevar.variance(db) == TypeVarVariance::Invariant - { - has_dynamic_replacement = true; - continue; - } - return false; - } - has_dynamic_replacement -} - /// Try to merge a complementary guarded pair into an unguarded core. /// /// e.g. @@ -1390,6 +1336,11 @@ impl<'db> IntersectionBuilder<'db> { } } +struct Replacement<'db> { + index: usize, + value: Type<'db>, +} + #[derive(Debug, Clone, Default)] struct InnerIntersectionBuilder<'db> { positive: FxOrderSet>, @@ -1630,25 +1581,26 @@ impl<'db> InnerIntersectionBuilder<'db> { } let mut to_remove = SmallVec::<[usize; 1]>::new(); + let mut replacement = None; for (index, existing_positive) in self.positive.iter().enumerate() { - // S & T = S if S <: T or T is an invariant-dynamic generalization of S. - if existing_positive.is_redundant_with(db, new_positive) - || is_invariant_dynamic_generalization_of( - db, - new_positive, - *existing_positive, - ) + if let Some(merged) = + generic_gradual_intersection(db, new_positive, *existing_positive) { + if merged == *existing_positive { + return; + } + replacement = Some(Replacement { + index, + value: merged, + }); + break; + } + // S & T = S if S <: T. + if existing_positive.is_redundant_with(db, new_positive) { return; } // same rule, reverse order - if new_positive.is_redundant_with(db, *existing_positive) - || is_invariant_dynamic_generalization_of( - db, - *existing_positive, - new_positive, - ) - { + if new_positive.is_redundant_with(db, *existing_positive) { to_remove.push(index); } // A & B = Never if A and B are disjoint @@ -1658,12 +1610,27 @@ impl<'db> InnerIntersectionBuilder<'db> { return; } } + if let Some(Replacement { index, value }) = replacement { + self.positive.swap_remove_index(index); + self.add_positive(db, value); + return; + } for index in to_remove.into_iter().rev() { self.positive.swap_remove_index(index); } let mut to_remove = SmallVec::<[usize; 1]>::new(); + let mut replacement_negatives = SmallVec::<[Type<'db>; 1]>::new(); for (index, existing_negative) in self.negative.iter().enumerate() { + // S & ~G = S & ~Bottom[G] & Any if G is a dynamic generalization of the + // fully-static specialization S. + if let Some(bottom) = + negated_generalization_bottom(db, *existing_negative, new_positive) + { + to_remove.push(index); + replacement_negatives.push(bottom); + continue; + } // S & ~T = Never if S <: T if new_positive.is_subtype_of(db, *existing_negative) { *self = Self::default(); @@ -1678,6 +1645,17 @@ impl<'db> InnerIntersectionBuilder<'db> { for index in to_remove.into_iter().rev() { self.negative.swap_remove_index(index); } + // For example, if we add `Co[P]` to an intersection containing `~Co[Any]`, + // replace that negative with `~Bottom[Co[Any]]` and add `Any`, producing + // `Co[P] & ~Bottom[Co[Any]] & Any`. See `generics/set_theoretic.md` for details. + if !replacement_negatives.is_empty() { + for bottom in replacement_negatives { + self.add_negative(db, bottom); + } + self.add_positive(db, Type::any()); + self.add_positive(db, new_positive); + return; + } self.positive.insert(new_positive); } @@ -1759,6 +1737,17 @@ impl<'db> InnerIntersectionBuilder<'db> { self.add_negative(db, Type::string_literal(db, "")); } _ => { + // For example, if we add `~Co[Any]` to an intersection containing `Co[P]`, + // replace that negative with `~Bottom[Co[Any]]` and add `Any`, producing + // `Co[P] & ~Bottom[Co[Any]] & Any`. See `generics/set_theoretic.md` for details. + if let Some(bottom) = self.positive.iter().find_map(|existing_positive| { + negated_generalization_bottom(db, new_negative, *existing_positive) + }) { + self.add_negative(db, bottom); + self.add_positive(db, Type::any()); + return; + } + let new_negative_enum = new_negative.as_enum_literal(); let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_negative) in self.negative.iter().enumerate() { diff --git a/crates/ty_python_semantic/src/types/set_theoretic/generic_gradual_intersections.rs b/crates/ty_python_semantic/src/types/set_theoretic/generic_gradual_intersections.rs new file mode 100644 index 00000000000000..e63bedad0714cd --- /dev/null +++ b/crates/ty_python_semantic/src/types/set_theoretic/generic_gradual_intersections.rs @@ -0,0 +1,219 @@ +use crate::Db; +use crate::types::generics::{Specialization, specialization_variance}; +use crate::types::tuple::FixedLengthTuple; +use crate::types::{ + IntersectionType, KnownClass, StaticClassLiteral, Type, TypeVarVariance, UnionType, +}; + +/// Simplify the intersection of two generic specializations when one is a gradual +/// generalization of the other. +/// +/// For example, `list[int] & list[Any]` simplifies to `list[int]`, while +/// `Sequence[int] & Sequence[Any]` simplifies to `Sequence[int & Any]`. +pub(super) fn generic_gradual_intersection<'db>( + db: &'db dyn Db, + left: Type<'db>, + right: Type<'db>, +) -> Option> { + dynamic_generalization_of(db, left, right) + .or_else(|| dynamic_generalization_of(db, right, left)) + .map(|generalization| generalization.intersection(db)) +} + +/// If `general` is a dynamic generalization of the fully-static `specific`, return the bottom +/// materialization that should replace `general` in `specific & ~general`. +/// +/// For example, `Co[P] & ~Co[Any] = Co[P] & ~Bottom[Co[Any]] & Any`; this function returns +/// `Bottom[Co[Any]]`. Similarly, `type[P] & ~type[Any] = type[P] & ~type[Never] & Any`; this +/// function returns `type[Never]`. +pub(super) fn negated_generalization_bottom<'db>( + db: &'db dyn Db, + general: Type<'db>, + specific: Type<'db>, +) -> Option> { + if let (Type::SubclassOf(general_subclass), Type::SubclassOf(_)) = (general, specific) + && general_subclass.is_dynamic() + && !specific.has_dynamic(db) + { + return Some(general.bottom_materialization(db)); + } + + dynamic_generalization_of(db, general, specific)?; + if specific.has_dynamic(db) { + None + } else { + Some(general.bottom_materialization(db)) + } +} + +/// Describes how intersecting a dynamic generic specialization such as `list[Any]` with a more +/// specific specialization such as `list[int]` should be simplified. +enum DynamicGeneralization<'db> { + /// All dynamic replacements occur in invariant or bivariant positions, so the intersection + /// simplifies to the original specific type. + /// + /// For example, `list[int] & list[Any] = list[int]`. + UseSpecific(Type<'db>), + /// At least one dynamic replacement occurs in a covariant or contravariant position, so the + /// intersection requires rebuilding the specialization according to each parameter's variance. + /// + /// For example, `Sequence[int] & Sequence[Any] = Sequence[int & Any]`. + RebuildSpecialization { + class: StaticClassLiteral<'db>, + general: Specialization<'db>, + specific: Specialization<'db>, + }, + /// Tuple specializations require rebuilding each element independently. + RebuildTuple { + general: &'db FixedLengthTuple>, + specific: &'db FixedLengthTuple>, + }, +} + +impl<'db> DynamicGeneralization<'db> { + /// Simplify the intersection represented by this relationship. + /// + /// Returns the original specific type when no reconstruction is needed, or rebuilds the + /// specialization according to its parameter variances. + fn intersection(self, db: &'db dyn Db) -> Type<'db> { + match self { + Self::UseSpecific(specific) => specific, + Self::RebuildTuple { general, specific } => Type::heterogeneous_tuple( + db, + specific + .iter_all_elements() + .zip(general.iter_all_elements()) + .map(|(specific, general)| { + IntersectionType::from_two_elements(db, specific, general) + }), + ), + Self::RebuildSpecialization { + class, + general, + specific, + } => { + let generic_context = general.generic_context(db); + let types: Vec<_> = generic_context + .variables(db) + .zip(general.types(db)) + .zip(specific.types(db)) + .map(|((typevar, general), specific)| { + if general == specific { + return *specific; + } + match specialization_variance(db, typevar) { + TypeVarVariance::Covariant => { + IntersectionType::from_two_elements(db, *specific, *general) + } + TypeVarVariance::Contravariant => { + UnionType::from_two_elements(db, *specific, *general) + } + TypeVarVariance::Invariant | TypeVarVariance::Bivariant => *specific, + } + }) + .collect(); + let specialization = generic_context.specialize(db, types); + + Type::instance( + db, + class.apply_optional_specialization(db, Some(specialization)), + ) + } + } + } +} + +/// Return the relationship between two specializations of the same generic class if `general` +/// only differs from `specific` by using dynamic types. +/// +/// For example, `list[Any]` dynamically generalizes `list[int]`, while `list[str]` does not. +fn dynamic_generalization_of<'db>( + db: &'db dyn Db, + general: Type<'db>, + specific: Type<'db>, +) -> Option> { + // Fast path to avoid performance regressions. + if !general.has_dynamic(db) + || matches!(general, Type::TypeVar(_) | Type::NewTypeInstance(_)) + || matches!(specific, Type::TypeVar(_) | Type::NewTypeInstance(_)) + { + return None; + } + + let ( + Some((general_class, general_specialization)), + Some((specific_class, specific_specialization)), + ) = ( + general.class_specialization(db), + specific.class_specialization(db), + ) + else { + return None; + }; + + // Top and bottom materializations are not gradual types. + if general_class != specific_class + || general_specialization == specific_specialization + || general_specialization.materialization_kind(db).is_some() + || specific_specialization.materialization_kind(db).is_some() + { + return None; + } + + if general_class.known(db) == Some(KnownClass::Tuple) { + let general_tuple = general_specialization.tuple(db)?.as_fixed_length()?; + let specific_tuple = specific_specialization.tuple(db)?.as_fixed_length()?; + if general_tuple.len() != specific_tuple.len() + || general_tuple + .iter_all_elements() + .zip(specific_tuple.iter_all_elements()) + .any(|(general, specific)| { + general != specific && !general.is_non_divergent_dynamic() + }) + { + return None; + } + if specific.has_dynamic(db) { + return None; + } + + return Some(DynamicGeneralization::RebuildTuple { + general: general_tuple, + specific: specific_tuple, + }); + } + + let generic_context = general_specialization.generic_context(db); + if generic_context + .variables(db) + .zip(general_specialization.types(db)) + .zip(specific_specialization.types(db)) + .any(|((_, general), specific)| general != specific && !general.is_non_divergent_dynamic()) + { + return None; + } + + if generic_context + .variables(db) + .zip(general_specialization.types(db)) + .zip(specific_specialization.types(db)) + .any(|((typevar, general), specific)| { + general != specific + && matches!( + specialization_variance(db, typevar), + TypeVarVariance::Covariant | TypeVarVariance::Contravariant + ) + }) + { + if specific.has_dynamic(db) { + return None; + } + Some(DynamicGeneralization::RebuildSpecialization { + class: general_class, + general: general_specialization, + specific: specific_specialization, + }) + } else { + Some(DynamicGeneralization::UseSpecific(specific)) + } +} diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 2ba2dcaef3b288..eaec0aa90ef21c 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -6,9 +6,9 @@ use crate::types::relation::{DisjointnessChecker, TypeRelationChecker}; use crate::types::variance::VarianceInferable; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, ClassType, DynamicType, - FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, MemberLookupPolicy, - SpecialFormType, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, TypeVarVariance, - TypedDictType, UnionType, todo_type, + FindLegacyTypeVarsVisitor, IntersectionBuilder, KnownClass, MaterializationKind, + MemberLookupPolicy, SpecialFormType, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, + TypeVarVariance, TypedDictType, UnionType, todo_type, }; use crate::{Db, FxOrderSet}; use ty_python_core::definition::Definition; @@ -78,8 +78,9 @@ impl<'db> SubclassOfType<'db> { /// Given an instance of the class or type variable `T`, returns a [`Type`] instance representing `type[T]`. pub(crate) fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option> { - // Handle unions by distributing `type[]` over each element: + // Handle unions and intersections by distributing `type[]` over each element: // `type[A | B]` -> `type[A] | type[B]` + // `type[A & B]` -> `type[A] & type[B]` match ty { Type::Union(union) => UnionType::try_from_elements( db, @@ -88,6 +89,14 @@ impl<'db> SubclassOfType<'db> { .iter() .map(|element| Self::try_from_instance(db, *element)), ), + Type::Intersection(intersection) if intersection.iter_negative(db).next().is_none() => { + intersection + .iter_positive(db) + .try_fold(IntersectionBuilder::new(db), |builder, element| { + Some(builder.add_positive(Self::try_from_instance(db, element)?)) + }) + .map(IntersectionBuilder::build) + } Type::ProtocolInstance(protocol) => Some(protocol.to_meta_type(db)), _ => SubclassOfInner::try_from_instance(db, ty) .map(|subclass_of| Self::from(db, subclass_of)),