diff --git a/crates/ty_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md index 4accdfe3685cf..256727dd2f745 100644 --- a/crates/ty_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md @@ -305,10 +305,13 @@ simplify to `Never`, even in the presence of other types: ```py from ty_extensions import Intersection, Not -from typing import Any +from typing import Any, Generic, TypeVar + +T_co = TypeVar("T_co", covariant=True) class P: ... class Q: ... +class R(Generic[T_co]): ... def _( i1: Intersection[P, Not[P]], @@ -317,6 +320,8 @@ def _( i4: Intersection[Not[P], Q, P], i5: Intersection[P, Any, Not[P]], i6: Intersection[Not[P], Any, P], + i7: Intersection[R[P], Not[R[P]]], + i8: Intersection[R[P], Not[R[Q]]], ) -> None: reveal_type(i1) # revealed: Never reveal_type(i2) # revealed: Never @@ -324,6 +329,8 @@ def _( reveal_type(i4) # revealed: Never reveal_type(i5) # revealed: Never reveal_type(i6) # revealed: Never + reveal_type(i7) # revealed: Never + reveal_type(i8) # revealed: R[P] & ~R[Q] ``` ### Union of a type and its negation @@ -332,20 +339,28 @@ Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `o ```py from ty_extensions import Intersection, Not +from typing import Generic, TypeVar + +T_co = TypeVar("T_co", covariant=True) class P: ... class Q: ... +class R(Generic[T_co]): ... def _( i1: P | Not[P], i2: Not[P] | P, i3: P | Q | Not[P], i4: Not[P] | Q | P, + i5: R[P] | Not[R[P]], + i6: R[P] | Not[R[Q]], ) -> None: reveal_type(i1) # revealed: object reveal_type(i2) # revealed: object reveal_type(i3) # revealed: object reveal_type(i4) # revealed: object + reveal_type(i5) # revealed: object + reveal_type(i6) # revealed: R[P] | ~R[Q] ``` ### Negation is an involution diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index c51916ece76db..51fe5e1b19a51 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -902,8 +902,7 @@ from ty_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf class HasX(Protocol): x: int -# TODO: this should pass -static_assert(is_subtype_of(TypeOf[module], HasX)) # error: [static-assert-error] +static_assert(is_subtype_of(TypeOf[module], HasX)) static_assert(is_assignable_to(TypeOf[module], HasX)) class ExplicitProtocolSubtype(HasX, Protocol): diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index a1e32e9ba7455..96993f1623588 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -209,6 +209,34 @@ class AnyMeta(metaclass=Any): ... static_assert(is_assignable_to(type[AnyMeta], type)) static_assert(is_assignable_to(type[AnyMeta], type[object])) static_assert(is_assignable_to(type[AnyMeta], type[Any])) + +from typing import TypeVar, Generic, Any + +T_co = TypeVar("T_co", covariant=True) + +class Foo(Generic[T_co]): ... +class Bar(Foo[T_co], Generic[T_co]): ... + +static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar], type[Foo])) +static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[Any]])) +static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]])) + +# TODO: these should pass (all subscripts inside `type[]` type expressions are currently TODO types) +static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) # error: [static-assert-error] +static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # error: [static-assert-error] +``` + +## `type[]` is not assignable to types disjoint from `builtins.type` + +```py +from typing import Any +from ty_extensions import is_assignable_to, static_assert + +static_assert(not is_assignable_to(type[Any], None)) ``` ## Class-literals that inherit from `Any` @@ -717,6 +745,53 @@ def f(x: int, y: str) -> None: ... c1: Callable[[int], None] = partial(f, y="a") ``` +### Generic classes with `__call__` + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing_extensions import Callable, Any, Generic, TypeVar, ParamSpec +from ty_extensions import static_assert, is_assignable_to + +T = TypeVar("T") +P = ParamSpec("P") + +class Foo[T]: + def __call__(self): ... + +class FooLegacy(Generic[T]): + def __call__(self): ... + +class Bar[T, **P]: + def __call__(self): ... + +# TODO: should not error +class BarLegacy(Generic[T, P]): # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`" + def __call__(self): ... + +static_assert(is_assignable_to(Foo, Callable[..., Any])) +static_assert(is_assignable_to(FooLegacy, Callable[..., Any])) +static_assert(is_assignable_to(Bar, Callable[..., Any])) +static_assert(is_assignable_to(BarLegacy, Callable[..., Any])) + +class Spam[T]: ... +class SpamLegacy(Generic[T]): ... +class Eggs[T, **P]: ... + +# TODO: should not error +class EggsLegacy(Generic[T, P]): ... # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`" + +static_assert(not is_assignable_to(Spam, Callable[..., Any])) +static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any])) +static_assert(not is_assignable_to(Eggs, Callable[..., Any])) + +# TODO: should pass +static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any])) # error: [static-assert-error] +``` + ### Classes with `__call__` as attribute An instance type is assignable to a compatible callable type if the instance type's class has a diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 22fa5316d5986..9a2d1b9243e2e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -611,6 +611,10 @@ impl<'db> Type<'db> { matches!(self, Type::GenericAlias(_)) } + const fn is_dynamic(&self) -> bool { + matches!(self, Type::Dynamic(_)) + } + /// Replace references to the class `class` with a self-reference marker. This is currently /// used for recursive protocols, but could probably be extended to self-referential type- /// aliases and similar. @@ -1050,34 +1054,26 @@ impl<'db> Type<'db> { /// /// [subtype of]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { - // Two equivalent types are always subtypes of each other. - // - // "Equivalent to" here means that the two types are both fully static - // and describe exactly the same set of possible runtime objects. - // For example, `int` is a subtype of `int` because `int` and `int` are equivalent to each other. - // Equally, `type[object]` is a subtype of `type`, - // because the former type expresses "all subclasses of `object`" - // while the latter expresses "all instances of `type`", - // and these are exactly the same set of objects at runtime. - if self.is_equivalent_to(db, target) { - return true; - } + self.has_relation_to(db, target, TypeRelation::Subtyping) + } - // Non-fully-static types do not participate in subtyping. - // - // Type `A` can only be a subtype of type `B` if the set of possible runtime objects - // that `A` represents is a subset of the set of possible runtime objects that `B` represents. - // But the set of objects described by a non-fully-static type is (either partially or wholly) unknown, - // so the question is simply unanswerable for non-fully-static types. - if !self.is_fully_static(db) || !target.is_fully_static(db) { + /// Return true if this type is [assignable to] type `target`. + /// + /// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation + pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { + self.has_relation_to(db, target, TypeRelation::Assignability) + } + + fn has_relation_to(self, db: &'db dyn Db, target: Type<'db>, relation: TypeRelation) -> bool { + if !relation.applies_to(db, self, target) { return false; } + if relation.are_equivalent(db, self, target) { + return true; + } match (self, target) { - // We should have handled these immediately above. - (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => { - unreachable!("Non-fully-static types do not participate in subtyping!") - } + (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => true, // `Never` is the bottom type, the empty set. // It is a subtype of all other fully static types. @@ -1115,12 +1111,12 @@ impl<'db> Type<'db> { match typevar.bound_or_constraints(db) { None => unreachable!(), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.is_subtype_of(db, target) + bound.has_relation_to(db, target, relation) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints .elements(db) .iter() - .all(|constraint| constraint.is_subtype_of(db, target)), + .all(|constraint| constraint.has_relation_to(db, target, relation)), } } @@ -1131,7 +1127,7 @@ impl<'db> Type<'db> { if typevar.constraints(db).is_some_and(|constraints| { constraints .iter() - .all(|constraint| self.is_subtype_of(db, *constraint)) + .all(|constraint| self.has_relation_to(db, *constraint, relation)) }) => { true @@ -1140,12 +1136,12 @@ impl<'db> Type<'db> { (Type::Union(union), _) => union .elements(db) .iter() - .all(|&elem_ty| elem_ty.is_subtype_of(db, target)), + .all(|&elem_ty| elem_ty.has_relation_to(db, target, relation)), (_, Type::Union(union)) => union .elements(db) .iter() - .any(|&elem_ty| self.is_subtype_of(db, elem_ty)), + .any(|&elem_ty| self.has_relation_to(db, elem_ty, relation)), // If both sides are intersections we need to handle the right side first // (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B, @@ -1154,7 +1150,7 @@ impl<'db> Type<'db> { intersection .positive(db) .iter() - .all(|&pos_ty| self.is_subtype_of(db, pos_ty)) + .all(|&pos_ty| self.has_relation_to(db, pos_ty, relation)) && intersection .negative(db) .iter() @@ -1164,7 +1160,7 @@ impl<'db> Type<'db> { (Type::Intersection(intersection), _) => intersection .positive(db) .iter() - .any(|&elem_ty| elem_ty.is_subtype_of(db, target)), + .any(|&elem_ty| elem_ty.has_relation_to(db, target, relation)), // Other than the special cases checked above, no other types are a subtype of a // typevar, since there's no guarantee what type the typevar will be specialized to. @@ -1179,7 +1175,7 @@ impl<'db> Type<'db> { (left, Type::AlwaysTruthy) => left.bool(db).is_always_true(), // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { - target.is_equivalent_to(db, Type::object(db)) + relation.are_equivalent(db, target, Type::object(db)) } // These clauses handle type variants that include function literals. A function @@ -1188,13 +1184,13 @@ impl<'db> Type<'db> { // applied to the signature. Different specializations of the same function literal are // only subtypes of each other if they result in the same signature. (Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => { - self_function.is_subtype_of(db, target_function) + self_function.has_relation_to(db, target_function, relation) } (Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => { - self_method.is_subtype_of(db, target_method) + self_method.has_relation_to(db, target_method, relation) } (Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => { - self_method.is_subtype_of(db, target_method) + self_method.has_relation_to(db, target_method, relation) } // No literal type is a subtype of any other literal type, unless they are the same @@ -1216,6 +1212,31 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(_), ) => false, + (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => { + let call_symbol = self + .member_lookup_with_policy( + db, + Name::new_static("__call__"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place; + // If the type of __call__ is a subtype of a callable type, this instance is. + // Don't add other special cases here; our subtyping of a callable type + // shouldn't get out of sync with the calls we will actually allow. + if let Place::Type(t, Boundness::Bound) = call_symbol { + t.has_relation_to(db, target, relation) + } else { + false + } + } + + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.has_relation_to(db, right, relation) + } + // A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`. + (Type::ProtocolInstance(_), _) => false, + (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol), + // All `StringLiteral` types are a subtype of `LiteralString`. (Type::StringLiteral(_), Type::LiteralString) => true, @@ -1231,45 +1252,37 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(_), _, ) => (self.literal_fallback_instance(db)) - .is_some_and(|instance| instance.is_subtype_of(db, target)), - - // Function-like callables are subtypes of `FunctionType` - (Type::Callable(callable), Type::NominalInstance(target)) - if callable.is_function_like(db) - && target.class.is_known(db, KnownClass::FunctionType) => - { - true - } + .is_some_and(|instance| instance.has_relation_to(db, target, relation)), (Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => { self_function_literal .into_callable_type(db) - .is_subtype_of(db, target) + .has_relation_to(db, target, relation) } (Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method .into_callable_type(db) - .is_subtype_of(db, target), + .has_relation_to(db, target, relation), // A `FunctionLiteral` type is a single-valued type like the other literals handled above, // so it also, for now, just delegates to its instance fallback. (Type::FunctionLiteral(_), _) => KnownClass::FunctionType .to_instance(db) - .is_subtype_of(db, target), + .has_relation_to(db, target, relation), // The same reasoning applies for these special callable types: (Type::BoundMethod(_), _) => KnownClass::MethodType .to_instance(db) - .is_subtype_of(db, target), + .has_relation_to(db, target, relation), (Type::MethodWrapper(_), _) => KnownClass::WrapperDescriptorType .to_instance(db) - .is_subtype_of(db, target), + .has_relation_to(db, target, relation), (Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType .to_instance(db) - .is_subtype_of(db, target), + .has_relation_to(db, target, relation), (Type::Callable(self_callable), Type::Callable(other_callable)) => { - self_callable.is_subtype_of(db, other_callable) + self_callable.has_relation_to(db, other_callable, relation) } (Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => { @@ -1277,29 +1290,15 @@ impl<'db> Type<'db> { false } - (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => { - let call_symbol = self - .member_lookup_with_policy( - db, - Name::new_static("__call__"), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place; - // If the type of __call__ is a subtype of a callable type, this instance is. - // Don't add other special cases here; our subtyping of a callable type - // shouldn't get out of sync with the calls we will actually allow. - if let Place::Type(t, Boundness::Bound) = call_symbol { - t.is_subtype_of(db, target) - } else { - false - } - } - (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { - left.is_subtype_of(db, right) + // Function-like callables are subtypes of `FunctionType` + (Type::Callable(callable), _) + if callable.is_function_like(db) + && KnownClass::FunctionType + .to_instance(db) + .has_relation_to(db, target, relation) => + { + true } - // A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`. - (Type::ProtocolInstance(_), _) => false, - (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol), (Type::Callable(_), _) => { // TODO: Implement subtyping between callable types and other types like @@ -1319,54 +1318,81 @@ impl<'db> Type<'db> { self_elements.len() == target_elements.len() && self_elements.iter().zip(target_elements).all( |(self_element, target_element)| { - self_element.is_subtype_of(db, *target_element) + self_element.has_relation_to(db, *target_element, relation) }, ) } // `tuple[A, B, C]` is a subtype of `tuple[A | B | C, ...]` - (Type::Tuple(tuple), _) => tuple.homogeneous_supertype(db).is_subtype_of(db, target), + (Type::Tuple(tuple), _) => tuple + .homogeneous_supertype(db) + .has_relation_to(db, target, relation), - (Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target), - (Type::BoundSuper(_), _) => KnownClass::Super.to_instance(db).is_subtype_of(db, target), + (Type::BoundSuper(_), Type::BoundSuper(_)) => relation.are_equivalent(db, self, target), + (Type::BoundSuper(_), _) => KnownClass::Super + .to_instance(db) + .has_relation_to(db, target, relation), // `Literal[]` is a subtype of `type[B]` if `C` is a subclass of `B`, // since `type[B]` describes all possible runtime subclasses of the class object `B`. (Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() .into_class() - .is_some_and(|target_class| class.is_subclass_of(db, None, target_class)), + .is_none_or(|subclass_of_class| { + ClassType::NonGeneric(class).has_relation_to(db, subclass_of_class, relation) + }), (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() .into_class() - .is_some_and(|target_class| { - ClassType::from(alias).is_subclass_of(db, target_class) + .is_none_or(|subclass_of_class| { + ClassType::Generic(alias).has_relation_to(db, subclass_of_class, relation) }), // This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`? (Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => { - self_subclass_ty.is_subtype_of(db, target_subclass_ty) + self_subclass_ty.has_relation_to(db, target_subclass_ty, relation) } (Type::ClassLiteral(class_literal), Type::Callable(_)) => { ClassType::NonGeneric(class_literal) .into_callable(db) - .is_subtype_of(db, target) + .has_relation_to(db, target, relation) } (Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias) .into_callable(db) - .is_subtype_of(db, target), + .has_relation_to(db, target, relation), // `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`. // `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object // is an instance of its metaclass `abc.ABCMeta`. - (Type::ClassLiteral(class), _) => { - class.metaclass_instance_type(db).is_subtype_of(db, target) - } + (Type::ClassLiteral(class), _) => class + .metaclass_instance_type(db) + .has_relation_to(db, target, relation), (Type::GenericAlias(alias), _) => ClassType::from(alias) .metaclass_instance_type(db) - .is_subtype_of(db, target), + .has_relation_to(db, target, relation), + + // This branch upholds two properties: + // - For any type `T` that is assignable to `type`, `T` shall be assignable to `type[Any]`. + // - For any type `T` that is assignable to `type`, `type[Any]` shall be assignable to `T`. + // + // This is really the same as the very first branch in this `match` statement that handles dynamic types. + // That branch upholds two properties: + // - For any type `S` that is assignable to `object` (which is _all_ types), `S` shall be assignable to `Any` + // - For any type `S` that is assignable to `object` (which is _all_ types), `Any` shall be assignable to `S`. + // + // The only difference between this branch and the first branch is that the first branch deals with the type + // `object & Any` (which simplifies to `Any`!) whereas this branch deals with the type `type & Any`. + // + // See also: + (Type::SubclassOf(subclass_of_ty), other) + | (other, Type::SubclassOf(subclass_of_ty)) + if subclass_of_ty.is_dynamic() + && other.has_relation_to(db, KnownClass::Type.to_instance(db), relation) => + { + true + } // `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses // of the class object `str`. It is a subtype of `type` (== `Instance("type")`) because `str` @@ -1379,30 +1405,31 @@ impl<'db> Type<'db> { .subclass_of() .into_class() .map(|class| class.metaclass_instance_type(db)) - .is_some_and(|metaclass_instance_type| { - metaclass_instance_type.is_subtype_of(db, target) - }), + .unwrap_or_else(|| KnownClass::Type.to_instance(db)) + .has_relation_to(db, target, relation), // For example: `Type::SpecialForm(SpecialFormType::Type)` is a subtype of `Type::NominalInstance(_SpecialForm)`, // because `Type::SpecialForm(SpecialFormType::Type)` is a set with exactly one runtime value in it // (the symbol `typing.Type`), and that symbol is known to be an instance of `typing._SpecialForm` at runtime. - (Type::SpecialForm(left), right) => left.instance_fallback(db).is_subtype_of(db, right), + (Type::SpecialForm(left), right) => left + .instance_fallback(db) + .has_relation_to(db, right, relation), - (Type::KnownInstance(left), right) => { - left.instance_fallback(db).is_subtype_of(db, right) - } + (Type::KnownInstance(left), right) => left + .instance_fallback(db) + .has_relation_to(db, right, relation), // `bool` is a subtype of `int`, because `bool` subclasses `int`, // which means that all instances of `bool` are also instances of `int` (Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => { - self_instance.is_subtype_of(db, target_instance) + self_instance.has_relation_to(db, target_instance, relation) } (Type::PropertyInstance(_), _) => KnownClass::Property .to_instance(db) - .is_subtype_of(db, target), + .has_relation_to(db, target, relation), (_, Type::PropertyInstance(_)) => { - self.is_subtype_of(db, KnownClass::Property.to_instance(db)) + self.has_relation_to(db, KnownClass::Property.to_instance(db), relation) } // Other than the special cases enumerated above, `Instance` types and typevars are @@ -1411,292 +1438,6 @@ impl<'db> Type<'db> { } } - /// Return true if this type is [assignable to] type `target`. - /// - /// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation - pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { - if self.is_gradual_equivalent_to(db, target) { - return true; - } - - match (self, target) { - // Never can be assigned to any type. - (Type::Never, _) => true, - - // The dynamic type is assignable-to and assignable-from any type. - (Type::Dynamic(_), _) => true, - (_, Type::Dynamic(_)) => true, - - // All types are assignable to `object`. - // TODO this special case might be removable once the below cases are comprehensive - (_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true, - - // In general, a TypeVar `T` is not assignable to a type `S` unless one of the two conditions is satisfied: - // 1. `T` is a bound TypeVar and `T`'s upper bound is assignable to `S`. - // TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`. - // 2. `T` is a constrained TypeVar and all of `T`'s constraints are assignable to `S`. - // - // However, there is one exception to this general rule: for any given typevar `T`, - // `T` will always be assignable to any union containing `T`. - // A similar rule applies in reverse to intersection types. - (Type::TypeVar(_), Type::Union(union)) if union.elements(db).contains(&self) => true, - (Type::Intersection(intersection), Type::TypeVar(_)) - if intersection.positive(db).contains(&target) => - { - true - } - (Type::Intersection(intersection), Type::TypeVar(_)) - if intersection.negative(db).contains(&target) => - { - false - } - - // A typevar is assignable to its upper bound, and to something similar to the union of - // its constraints. An unbound, unconstrained typevar has an implicit upper bound of - // `object` (which is handled above). - (Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => { - match typevar.bound_or_constraints(db) { - None => unreachable!(), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.is_assignable_to(db, target) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_assignable_to(db, target)), - } - } - - // If the typevar is constrained, there must be multiple constraints, and the typevar - // might be specialized to any one of them. However, the constraints do not have to be - // disjoint, which means an lhs type might be assignable to all of the constraints. - (_, Type::TypeVar(typevar)) - if typevar.constraints(db).is_some_and(|constraints| { - constraints - .iter() - .all(|constraint| self.is_assignable_to(db, *constraint)) - }) => - { - true - } - - // A union is assignable to a type T iff every element of the union is assignable to T. - (Type::Union(union), ty) => union - .elements(db) - .iter() - .all(|&elem_ty| elem_ty.is_assignable_to(db, ty)), - - // A type T is assignable to a union iff T is assignable to any element of the union. - (ty, Type::Union(union)) => union - .elements(db) - .iter() - .any(|&elem_ty| ty.is_assignable_to(db, elem_ty)), - - // If both sides are intersections we need to handle the right side first - // (A & B & C) is assignable to (A & B) because the left is assignable to both A and B, - // but none of A, B, or C is assignable to (A & B). - // - // A type S is assignable to an intersection type T if - // S is assignable to all positive elements of T (e.g. `str & int` is assignable to `str & Any`), and - // S is disjoint from all negative elements of T (e.g. `int` is not assignable to Intersection[int, Not[Literal[1]]]). - (ty, Type::Intersection(intersection)) => { - intersection - .positive(db) - .iter() - .all(|&elem_ty| ty.is_assignable_to(db, elem_ty)) - && intersection - .negative(db) - .iter() - .all(|&neg_ty| ty.is_disjoint_from(db, neg_ty)) - } - - // An intersection type S is assignable to a type T if - // Any element of S is assignable to T (e.g. `A & B` is assignable to `A`) - // Negative elements do not have an effect on assignability - if S is assignable to T then S & ~P is also assignable to T. - (Type::Intersection(intersection), ty) => intersection - .positive(db) - .iter() - .any(|&elem_ty| elem_ty.is_assignable_to(db, ty)), - - // Other than the special cases checked above, no other types are assignable to a - // typevar, since there's no guarantee what type the typevar will be specialized to. - // (If the typevar is bounded, it might be specialized to a smaller type than the - // bound. This is true even if the bound is a final class, since the typevar can still - // be specialized to `Never`.) - (_, Type::TypeVar(_)) => false, - - // A tuple type S is assignable to a tuple type T if their lengths are the same, and - // each element of S is assignable to the corresponding element of T. - (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { - let self_elements = self_tuple.elements(db); - let target_elements = target_tuple.elements(db); - self_elements.len() == target_elements.len() - && self_elements.iter().zip(target_elements).all( - |(self_element, target_element)| { - self_element.is_assignable_to(db, *target_element) - }, - ) - } - - // This special case is required because the left-hand side tuple might be a - // gradual type, so we can not rely on subtyping. This allows us to assign e.g. - // `tuple[Any, int]` to `tuple`. - // - // `tuple[A, B, C]` is assignable to `tuple[A | B | C, ...]` - (Type::Tuple(tuple), _) - if tuple.homogeneous_supertype(db).is_assignable_to(db, target) => - { - true - } - - // These clauses handle type variants that include function literals. A function - // literal is assignable to itself, and not to any other function literal. However, our - // representation of a function literal includes any specialization that should be - // applied to the signature. Different specializations of the same function literal are - // only assignable to each other if they result in the same signature. - (Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => { - self_function.is_assignable_to(db, target_function) - } - (Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => { - self_method.is_assignable_to(db, target_method) - } - (Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => { - self_method.is_assignable_to(db, target_method) - } - - // `type[Any]` is assignable to any `type[...]` type, because `type[Any]` can - // materialize to any `type[...]` type. - (Type::SubclassOf(subclass_of_ty), Type::SubclassOf(_)) - if subclass_of_ty.is_dynamic() => - { - true - } - - (Type::ClassLiteral(class), Type::SubclassOf(_)) - if class - .iter_mro(db, None) - .any(class_base::ClassBase::is_dynamic) => - { - true - } - - // Every `type[...]` is assignable to `type` - (Type::SubclassOf(_), _) - if KnownClass::Type - .to_instance(db) - .is_assignable_to(db, target) => - { - true - } - - // All `type[...]` types are assignable to `type[Any]`, because `type[Any]` can - // materialize to any `type[...]` type. - // - // Every class literal type is also assignable to `type[Any]`, because the class - // literal type for a class `C` is a subtype of `type[C]`, and `type[C]` is assignable - // to `type[Any]`. - ( - Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_), - Type::SubclassOf(target_subclass_of), - ) if target_subclass_of.is_dynamic() => true, - - // `type[Any]` is assignable to any type that `type[object]` is assignable to, because - // `type[Any]` can materialize to `type[object]`. - // - // `type[Any]` is also assignable to any subtype of `type[object]`, because all - // subtypes of `type[object]` are `type[...]` types (or `Never`), and `type[Any]` can - // materialize to any `type[...]` type (or to `type[Never]`, which is equivalent to - // `Never`.) - (Type::SubclassOf(subclass_of_ty), Type::NominalInstance(_)) - if subclass_of_ty.is_dynamic() - && (KnownClass::Type - .to_instance(db) - .is_assignable_to(db, target) - || target.is_subtype_of(db, KnownClass::Type.to_instance(db))) => - { - true - } - - // Any type that is assignable to `type[object]` is also assignable to `type[Any]`, - // because `type[Any]` can materialize to `type[object]`. - (Type::NominalInstance(_), Type::SubclassOf(subclass_of_ty)) - if subclass_of_ty.is_dynamic() - && self.is_assignable_to(db, KnownClass::Type.to_instance(db)) => - { - true - } - - (Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => { - self_instance.is_assignable_to(db, target_instance) - } - - (Type::Callable(self_callable), Type::Callable(target_callable)) => { - self_callable.is_assignable_to(db, target_callable) - } - - (Type::NominalInstance(instance), Type::Callable(_)) - if instance.class.is_subclass_of_any_or_unknown(db) => - { - true - } - - (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => { - let call_symbol = self - .member_lookup_with_policy( - db, - Name::new_static("__call__"), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place; - // shouldn't get out of sync with the calls we will actually allow. - if let Place::Type(t, Boundness::Bound) = call_symbol { - t.is_assignable_to(db, target) - } else { - false - } - } - - _ if self - .literal_fallback_instance(db) - .is_some_and(|instance| instance.is_assignable_to(db, target)) => - { - true - } - - (Type::ClassLiteral(class_literal), Type::Callable(_)) => { - ClassType::NonGeneric(class_literal) - .into_callable(db) - .is_assignable_to(db, target) - } - - (Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias) - .into_callable(db) - .is_assignable_to(db, target), - - (Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => { - self_function_literal - .into_callable_type(db) - .is_assignable_to(db, target) - } - - (Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method - .into_callable_type(db) - .is_assignable_to(db, target), - - (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { - left.is_assignable_to(db, right) - } - // Other than the dynamic types such as `Any`/`Unknown`/`Todo` handled above, - // a protocol instance can never be assignable to a nominal type, - // with the *sole* exception of `object`. - (Type::ProtocolInstance(_), _) => false, - (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol), - - // TODO other types containing gradual forms - _ => self.is_subtype_of(db, target), - } - } - /// Return true if this type is [equivalent to] type `other`. /// /// This method returns `false` if either `self` or `other` is not fully static. @@ -7027,6 +6768,45 @@ impl<'db> ConstructorCallError<'db> { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) enum TypeRelation { + Subtyping, + Assignability, +} + +impl TypeRelation { + /// Non-fully-static types do not participate in subtyping, only assignability, + /// so the subtyping relation does not even apply to them. + /// + /// Type `A` can only be a subtype of type `B` if the set of possible runtime objects + /// that `A` represents is a subset of the set of possible runtime objects that `B` represents. + /// But the set of objects described by a non-fully-static type is (either partially or wholly) unknown, + /// so the question is simply unanswerable for non-fully-static types. + /// + /// However, the assignability relation applies to all types, even non-fully-static ones. + fn applies_to<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool { + match self { + TypeRelation::Subtyping => type_1.is_fully_static(db) && type_2.is_fully_static(db), + TypeRelation::Assignability => true, + } + } + + /// Determine whether `type_1` and `type_2` are equivalent. + /// + /// Depending on whether the context is a subtyping test or an assignability test, + /// this method may call [`Type::is_equivalent_to`] or [`Type::is_assignable_to`]. + fn are_equivalent<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool { + match self { + TypeRelation::Subtyping => type_1.is_equivalent_to(db, type_2), + TypeRelation::Assignability => type_1.is_gradual_equivalent_to(db, type_2), + } + } + + const fn applies_to_non_fully_static_types(self) -> bool { + matches!(self, TypeRelation::Assignability) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Truthiness { /// For an object `x`, `bool(x)` will always return `True` @@ -7139,26 +6919,16 @@ impl<'db> BoundMethodType<'db> { ) } - fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { + fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool { // A bound method is a typically a subtype of itself. However, we must explicitly verify // the subtyping of the underlying function signatures (since they might be specialized // differently), and of the bound self parameter (taking care that parameters, including a // bound self parameter, are contravariant.) - self.function(db).is_subtype_of(db, other.function(db)) - && other - .self_instance(db) - .is_subtype_of(db, self.self_instance(db)) - } - - fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { - // A bound method is a typically assignable to itself. However, we must explicitly verify - // the assignability of the underlying function signatures (since they might be specialized - // differently), and of the bound self parameter (taking care that parameters, including a - // bound self parameter, are contravariant.) - self.function(db).is_assignable_to(db, other.function(db)) + self.function(db) + .has_relation_to(db, other.function(db), relation) && other .self_instance(db) - .is_assignable_to(db, self.self_instance(db)) + .has_relation_to(db, self.self_instance(db), relation) } fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { @@ -7276,26 +7046,15 @@ impl<'db> CallableType<'db> { self.signatures(db).is_fully_static(db) } - /// Check whether this callable type is a subtype of another callable type. + /// Check whether this callable type has the given relation to another callable type. /// - /// See [`Type::is_subtype_of`] for more details. - fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { - let self_is_function_like = self.is_function_like(db); - let other_is_function_like = other.is_function_like(db); - (self_is_function_like || !other_is_function_like) - && self.signatures(db).is_subtype_of(db, other.signatures(db)) - } - - /// Check whether this callable type is assignable to another callable type. - /// - /// See [`Type::is_assignable_to`] for more details. - fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { - let self_is_function_like = self.is_function_like(db); - let other_is_function_like = other.is_function_like(db); - (self_is_function_like || !other_is_function_like) - && self - .signatures(db) - .is_assignable_to(db, other.signatures(db)) + /// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details. + fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool { + if other.is_function_like(db) && !self.is_function_like(db) { + return false; + } + self.signatures(db) + .has_relation_to(db, other.signatures(db), relation) } /// Check whether this callable type is equivalent to another callable type. @@ -7348,50 +7107,17 @@ pub enum MethodWrapperKind<'db> { } impl<'db> MethodWrapperKind<'db> { - fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { - match (self, other) { - ( - MethodWrapperKind::FunctionTypeDunderGet(self_function), - MethodWrapperKind::FunctionTypeDunderGet(other_function), - ) => self_function.is_subtype_of(db, other_function), - - ( - MethodWrapperKind::FunctionTypeDunderCall(self_function), - MethodWrapperKind::FunctionTypeDunderCall(other_function), - ) => self_function.is_subtype_of(db, other_function), - - (MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_)) - | (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_)) - | (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => { - self == other - } - - ( - MethodWrapperKind::FunctionTypeDunderGet(_) - | MethodWrapperKind::FunctionTypeDunderCall(_) - | MethodWrapperKind::PropertyDunderGet(_) - | MethodWrapperKind::PropertyDunderSet(_) - | MethodWrapperKind::StrStartswith(_), - MethodWrapperKind::FunctionTypeDunderGet(_) - | MethodWrapperKind::FunctionTypeDunderCall(_) - | MethodWrapperKind::PropertyDunderGet(_) - | MethodWrapperKind::PropertyDunderSet(_) - | MethodWrapperKind::StrStartswith(_), - ) => false, - } - } - - fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { + fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool { match (self, other) { ( MethodWrapperKind::FunctionTypeDunderGet(self_function), MethodWrapperKind::FunctionTypeDunderGet(other_function), - ) => self_function.is_assignable_to(db, other_function), + ) => self_function.has_relation_to(db, other_function, relation), ( MethodWrapperKind::FunctionTypeDunderCall(self_function), MethodWrapperKind::FunctionTypeDunderCall(other_function), - ) => self_function.is_assignable_to(db, other_function), + ) => self_function.has_relation_to(db, other_function, relation), (MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_)) | (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_)) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 344c2002379e2..bf5e6f494af7f 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -12,7 +12,7 @@ use crate::types::function::{DataclassTransformerParams, KnownFunction}; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; use crate::types::{ - CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeVarInstance, + CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeRelation, TypeVarInstance, }; use crate::{ Db, FxOrderSet, KnownModule, Program, @@ -29,8 +29,8 @@ use crate::{ place_table, semantic_index, use_def_map, }, types::{ - CallArgumentTypes, CallError, CallErrorKind, DynamicType, MetaclassCandidate, TupleType, - UnionBuilder, UnionType, definition_expression_type, + CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, TupleType, UnionBuilder, + UnionType, definition_expression_type, }, }; use indexmap::IndexSet; @@ -340,23 +340,22 @@ impl<'db> ClassType<'db> { class_literal.is_final(db) } - /// Is this class a subclass of `Any` or `Unknown`? - pub(crate) fn is_subclass_of_any_or_unknown(self, db: &'db dyn Db) -> bool { - self.iter_mro(db).any(|base| { - matches!( - base, - ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown) - ) - }) - } - /// Return `true` if `other` is present in this class's MRO. pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + self.has_relation_to(db, other, TypeRelation::Subtyping) + } + + pub(super) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { self.iter_mro(db).any(|base| { match base { - // `is_subclass_of` is checking the subtype relation, in which gradual types do not - // participate. - ClassBase::Dynamic(_) => false, + ClassBase::Dynamic(_) => { + relation.applies_to_non_fully_static_types() && !other.is_final(db) + } // Protocol and Generic are not represented by a ClassType. ClassBase::Protocol | ClassBase::Generic => false, @@ -365,9 +364,11 @@ impl<'db> ClassType<'db> { (ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other, (ClassType::Generic(base), ClassType::Generic(other)) => { base.origin(db) == other.origin(db) - && base - .specialization(db) - .is_subtype_of(db, other.specialization(db)) + && base.specialization(db).has_relation_to( + db, + other.specialization(db), + relation, + ) } (ClassType::Generic(_), ClassType::NonGeneric(_)) | (ClassType::NonGeneric(_), ClassType::Generic(_)) => false, @@ -390,30 +391,6 @@ impl<'db> ClassType<'db> { } } - pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - self.iter_mro(db).any(|base| { - match base { - ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown) => !other.is_final(db), - ClassBase::Dynamic(_) => false, - - // Protocol and Generic are not represented by a ClassType. - ClassBase::Protocol | ClassBase::Generic => false, - - ClassBase::Class(base) => match (base, other) { - (ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other, - (ClassType::Generic(base), ClassType::Generic(other)) => { - base.origin(db) == other.origin(db) - && base - .specialization(db) - .is_assignable_to(db, other.specialization(db)) - } - (ClassType::Generic(_), ClassType::NonGeneric(_)) - | (ClassType::NonGeneric(_), ClassType::Generic(_)) => false, - }, - } - }) - } - pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { match (self, other) { (ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other, diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index cf13ee5b38453..8e6e27a7efe26 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -279,10 +279,6 @@ impl<'db> ClassBase<'db> { } } } - - pub(crate) const fn is_dynamic(self) -> bool { - matches!(self, Self::Dynamic(_)) - } } impl<'db> From> for ClassBase<'db> { diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index c6164e6d929ca..144a508b03d65 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -67,7 +67,9 @@ use crate::semantic_index::semantic_index; use crate::types::generics::GenericContext; use crate::types::narrow::ClassInfoConstraintFunction; use crate::types::signatures::{CallableSignature, Signature}; -use crate::types::{BoundMethodType, CallableType, Type, TypeMapping, TypeVarInstance}; +use crate::types::{ + BoundMethodType, CallableType, Type, TypeMapping, TypeRelation, TypeVarInstance, +}; use crate::{Db, FxOrderSet}; /// A collection of useful spans for annotating functions. @@ -707,6 +709,18 @@ impl<'db> FunctionType<'db> { Type::BoundMethod(BoundMethodType::new(db, self, self_instance)) } + pub(crate) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { + match relation { + TypeRelation::Subtyping => self.is_subtype_of(db, other), + TypeRelation::Assignability => self.is_assignable_to(db, other), + } + } + pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { // A function type is the subtype of itself, and not of any other function type. However, // our representation of a function type includes any specialization that should be applied diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 4866b73efdabf..a4ebf02a6987c 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -9,7 +9,7 @@ use crate::types::class_base::ClassBase; use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType}; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ - KnownInstanceType, Type, TypeMapping, TypeVarBoundOrConstraints, TypeVarInstance, + KnownInstanceType, Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType, declaration_type, todo_type, }; use crate::{Db, FxOrderSet}; @@ -358,7 +358,12 @@ impl<'db> Specialization<'db> { Self::new(db, self.generic_context(db), types) } - pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Specialization<'db>) -> bool { + pub(crate) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { let generic_context = self.generic_context(db); if generic_context != other.generic_context(db) { return false; @@ -368,20 +373,31 @@ impl<'db> Specialization<'db> { .zip(self.types(db)) .zip(other.types(db)) { - if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) { - return false; + if self_type.is_dynamic() || other_type.is_dynamic() { + match relation { + TypeRelation::Assignability => continue, + TypeRelation::Subtyping => return false, + } } - // Subtyping of each type in the specialization depends on the variance of the - // corresponding typevar: + // 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 - // - bivariant: skip, can't make subtyping false + // - invariant: verify that self_type <: other_type AND other_type <: self_type + // - bivariant: skip, can't make subtyping/assignability false let compatible = match typevar.variance(db) { - TypeVarVariance::Invariant => self_type.is_equivalent_to(db, *other_type), - TypeVarVariance::Covariant => self_type.is_subtype_of(db, *other_type), - TypeVarVariance::Contravariant => other_type.is_subtype_of(db, *self_type), + TypeVarVariance::Invariant => match relation { + TypeRelation::Subtyping => self_type.is_equivalent_to(db, *other_type), + TypeRelation::Assignability => { + self_type.is_assignable_to(db, *other_type) + && other_type.is_assignable_to(db, *self_type) + } + }, + TypeVarVariance::Covariant => self_type.has_relation_to(db, *other_type, relation), + TypeVarVariance::Contravariant => { + other_type.has_relation_to(db, *self_type, relation) + } TypeVarVariance::Bivariant => true, }; if !compatible { @@ -426,43 +442,6 @@ impl<'db> Specialization<'db> { true } - pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool { - let generic_context = self.generic_context(db); - if generic_context != other.generic_context(db) { - return false; - } - - for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) - .zip(self.types(db)) - .zip(other.types(db)) - { - if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) { - continue; - } - - // 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 assignability false - let compatible = match typevar.variance(db) { - TypeVarVariance::Invariant => { - self_type.is_assignable_to(db, *other_type) - && other_type.is_assignable_to(db, *self_type) - } - TypeVarVariance::Covariant => self_type.is_assignable_to(db, *other_type), - TypeVarVariance::Contravariant => other_type.is_assignable_to(db, *self_type), - TypeVarVariance::Bivariant => true, - }; - if !compatible { - return false; - } - } - - true - } - pub(crate) fn is_gradual_equivalent_to( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index c62c6f0b4f6b8..40ba5d671972d 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -5,14 +5,16 @@ use std::marker::PhantomData; use super::protocol_class::ProtocolInterface; use super::{ClassType, KnownClass, SubclassOfType, Type}; use crate::place::{Boundness, Place, PlaceAndQualifiers}; -use crate::types::{ClassLiteral, TypeMapping, TypeVarInstance}; +use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance}; use crate::{Db, FxOrderSet}; pub(super) use synthesized_protocol::SynthesizedProtocolType; impl<'db> Type<'db> { pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { - if class.class_literal(db).0.is_protocol(db) { + if class.is_known(db, KnownClass::Any) { + Self::Dynamic(DynamicType::Any) + } else if class.class_literal(db).0.is_protocol(db) { Self::ProtocolInstance(ProtocolInstanceType::from_class(class)) } else { Self::NominalInstance(NominalInstanceType::from_class(class)) @@ -78,19 +80,19 @@ impl<'db> NominalInstanceType<'db> { Self::from_class(self.class.normalized(db)) } - pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { - // N.B. The subclass relation is fully static - self.class.is_subclass_of(db, other.class) + pub(super) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { + self.class.has_relation_to(db, other.class, relation) } pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { self.class.is_equivalent_to(db, other.class) } - pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { - self.class.is_assignable_to(db, other.class) - } - pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool { if self.class.is_final(db) && !self.class.is_subclass_of(db, other.class) { return true; @@ -254,16 +256,20 @@ impl<'db> ProtocolInstanceType<'db> { self.inner.interface(db).is_fully_static(db) } - /// Return `true` if this protocol type is a subtype of the protocol `other`. - pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { - self.is_fully_static(db) && other.is_fully_static(db) && self.is_assignable_to(db, other) - } - - /// Return `true` if this protocol type is assignable to the protocol `other`. + /// Return `true` if this protocol type has the given type relation to the protocol `other`. /// /// TODO: consider the types of the members as well as their existence - pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { - other + pub(super) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { + relation.applies_to( + db, + Type::ProtocolInstance(self), + Type::ProtocolInstance(other), + ) && other .inner .interface(db) .is_sub_interface_of(db, self.inner.interface(db)) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 1f440f782f576..0e17986b1eee6 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -18,7 +18,7 @@ use smallvec::{SmallVec, smallvec}; use super::{DynamicType, Type, definition_expression_type}; use crate::semantic_index::definition::Definition; use crate::types::generics::GenericContext; -use crate::types::{ClassLiteral, TypeMapping, TypeVarInstance, todo_type}; +use crate::types::{ClassLiteral, TypeMapping, TypeRelation, TypeVarInstance, todo_type}; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -98,11 +98,23 @@ impl<'db> CallableSignature<'db> { .all(|signature| signature.is_fully_static(db)) } + pub(crate) fn has_relation_to( + &self, + db: &'db dyn Db, + other: &Self, + relation: TypeRelation, + ) -> bool { + match relation { + TypeRelation::Subtyping => self.is_subtype_of(db, other), + TypeRelation::Assignability => self.is_assignable_to(db, other), + } + } + /// Check whether this callable type is a subtype of another callable type. /// /// See [`Type::is_subtype_of`] for more details. pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Self) -> bool { - Self::is_assignable_to_impl( + Self::has_relation_to_impl( &self.overloads, &other.overloads, &|self_signature, other_signature| self_signature.is_subtype_of(db, other_signature), @@ -113,7 +125,7 @@ impl<'db> CallableSignature<'db> { /// /// See [`Type::is_assignable_to`] for more details. pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Self) -> bool { - Self::is_assignable_to_impl( + Self::has_relation_to_impl( &self.overloads, &other.overloads, &|self_signature, other_signature| self_signature.is_assignable_to(db, other_signature), @@ -124,7 +136,7 @@ impl<'db> CallableSignature<'db> { /// types. /// /// The `check_signature` closure is used to check the relation between two [`Signature`]s. - fn is_assignable_to_impl( + fn has_relation_to_impl( self_signatures: &[Signature<'db>], other_signatures: &[Signature<'db>], check_signature: &F, @@ -140,7 +152,7 @@ impl<'db> CallableSignature<'db> { // `self` is possibly overloaded while `other` is definitely not overloaded. (_, [_]) => self_signatures.iter().any(|self_signature| { - Self::is_assignable_to_impl( + Self::has_relation_to_impl( std::slice::from_ref(self_signature), other_signatures, check_signature, @@ -149,7 +161,7 @@ impl<'db> CallableSignature<'db> { // `self` is definitely not overloaded while `other` is possibly overloaded. ([_], _) => other_signatures.iter().all(|other_signature| { - Self::is_assignable_to_impl( + Self::has_relation_to_impl( self_signatures, std::slice::from_ref(other_signature), check_signature, @@ -158,7 +170,7 @@ impl<'db> CallableSignature<'db> { // `self` is definitely overloaded while `other` is possibly overloaded. (_, _) => other_signatures.iter().all(|other_signature| { - Self::is_assignable_to_impl( + Self::has_relation_to_impl( self_signatures, std::slice::from_ref(other_signature), check_signature, diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index b2febf439be6c..2143d2e1e25bb 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -1,6 +1,7 @@ use crate::place::PlaceAndQualifiers; use crate::types::{ - ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeVarInstance, + ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeRelation, + TypeVarInstance, }; use crate::{Db, FxOrderSet}; @@ -30,10 +31,14 @@ impl<'db> SubclassOfType<'db> { SubclassOfInner::Class(class) => { if class.is_final(db) { Type::from(class) - } else if class.is_object(db) { - KnownClass::Type.to_instance(db) } else { - Type::SubclassOf(Self { subclass_of }) + match class.known(db) { + Some(KnownClass::Object) => KnownClass::Type.to_instance(db), + Some(KnownClass::Any) => Type::SubclassOf(Self { + subclass_of: SubclassOfInner::Dynamic(DynamicType::Any), + }), + _ => Type::SubclassOf(Self { subclass_of }), + } } } } @@ -103,21 +108,23 @@ impl<'db> SubclassOfType<'db> { Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy) } - /// Return `true` if `self` is a subtype of `other`. - /// - /// This can only return `true` if `self.subclass_of` is a [`SubclassOfInner::Class`] variant; - /// only fully static types participate in subtyping. - pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool { + /// Return `true` if `self` has a certain relation to `other`. + pub(crate) fn has_relation_to( + self, + db: &'db dyn Db, + other: SubclassOfType<'db>, + relation: TypeRelation, + ) -> bool { match (self.subclass_of, other.subclass_of) { - // Non-fully-static types do not participate in subtyping - (SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => false, + (SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => { + relation.applies_to_non_fully_static_types() + } // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, // and `type[int]` describes all possible runtime subclasses of the class `int`. // The first set is a subset of the second set, because `bool` is itself a subclass of `int`. (SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => { - // N.B. The subclass relation is fully static - self_class.is_subclass_of(db, other_class) + self_class.has_relation_to(db, other_class, relation) } } }