diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 65c4f98865..ee5bffcb19 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -71,6 +71,18 @@ type ListOrSet[T] = list[T] | set[T] reveal_type(ListOrSet.__type_params__) # revealed: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] ``` +## In unions and intersections + +We can "break apart" a type alias by e.g. adding it to a union: + +```py +type IntOrStr = int | str + +def f(x: IntOrStr, y: str | bytes): + z = x or y + reveal_type(z) # revealed: (int & ~AlwaysFalsy) | str | bytes +``` + ## `TypeAliasType` properties Two `TypeAliasType`s are distinct and disjoint, even if they refer to the same type @@ -138,3 +150,50 @@ def get_name() -> str: # error: [invalid-type-alias-type] "The name of a `typing.TypeAlias` must be a string literal" IntOrStr = TypeAliasType(get_name(), int | str) ``` + +## Cyclic aliases + +### Self-referential + +```py +type OptNestedInt = int | tuple[OptNestedInt, ...] | None + +def f(x: OptNestedInt) -> None: + reveal_type(x) # revealed: int | tuple[OptNestedInt, ...] | None + if x is not None: + reveal_type(x) # revealed: int | tuple[OptNestedInt, ...] +``` + +### Invalid self-referential + +```py +# TODO emit a diagnostic here +type IntOr = int | IntOr + +def f(x: IntOr): + reveal_type(x) # revealed: int + if not isinstance(x, int): + reveal_type(x) # revealed: Never +``` + +### Mutually recursive + +```py +type A = tuple[B] | None +type B = tuple[A] | None + +def f(x: A): + if x is not None: + reveal_type(x) # revealed: tuple[B] + y = x[0] + if y is not None: + reveal_type(y) # revealed: tuple[A] + +def g(x: A | B): + reveal_type(x) # revealed: tuple[B] | None + +from ty_extensions import Intersection + +def h(x: Intersection[A, B]): + reveal_type(x) # revealed: tuple[B] | None +``` diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 220e808c30..52587fe60f 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -243,6 +243,7 @@ impl<'db> Completion<'db> { | Type::KnownInstance(_) | Type::AlwaysTruthy | Type::AlwaysFalsy => return None, + Type::TypeAlias(alias) => imp(db, alias.value_type(db))?, }) } imp(db, self.ty) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index fbbc0e0b36..9cead82223 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -605,6 +605,8 @@ pub enum Type<'db> { TypeIs(TypeIsType<'db>), /// A type that represents an inhabitant of a `TypedDict`. TypedDict(TypedDictType<'db>), + /// An aliased type (lazily not-yet-unpacked to its value type). + TypeAlias(TypeAliasType<'db>), } #[salsa::tracked] @@ -810,6 +812,7 @@ impl<'db> Type<'db> { // TODO: Materialization of gradual TypedDicts *self } + Type::TypeAlias(alias) => alias.value_type(db).materialize(db, variance), } } @@ -1119,12 +1122,11 @@ impl<'db> Type<'db> { // Always normalize single-member enums to their class instance (`Literal[Single.VALUE]` => `Single`) enum_literal.enum_class_instance(db) } - Type::TypedDict(_) => { // TODO: Normalize TypedDicts self } - + Type::TypeAlias(alias) => alias.value_type(db).normalized_impl(db, visitor), Type::LiteralString | Type::AlwaysFalsy | Type::AlwaysTruthy @@ -1186,7 +1188,8 @@ impl<'db> Type<'db> { | Type::TypeVar(_) | Type::BoundSuper(_) | Type::TypeIs(_) - | Type::TypedDict(_) => false, + | Type::TypedDict(_) + | Type::TypeAlias(_) => false, } } @@ -1239,6 +1242,8 @@ impl<'db> Type<'db> { enum_literal.enum_class_instance(db).into_callable(db) } + Type::TypeAlias(alias) => alias.value_type(db).into_callable(db), + Type::Never | Type::DataclassTransformer(_) | Type::AlwaysTruthy @@ -1313,6 +1318,16 @@ impl<'db> Type<'db> { } fn has_relation_to(self, db: &'db dyn Db, target: Type<'db>, relation: TypeRelation) -> bool { + self.has_relation_to_impl(db, target, relation, &PairVisitor::new(true)) + } + + fn has_relation_to_impl( + self, + db: &'db dyn Db, + target: Type<'db>, + relation: TypeRelation, + visitor: &PairVisitor<'db>, + ) -> bool { // Subtyping implies assignability, so if subtyping is reflexive and the two types are // equal, it is both a subtype and assignable. Assignability is always reflexive. // @@ -1334,6 +1349,16 @@ impl<'db> Type<'db> { // handled above. It's always assignable, though. (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => relation.is_assignability(), + (Type::TypeAlias(self_alias), _) => visitor.visit((self, target), || { + self_alias + .value_type(db) + .has_relation_to_impl(db, target, relation, visitor) + }), + + (_, Type::TypeAlias(target_alias)) => visitor.visit((self, target), || { + self.has_relation_to_impl(db, target_alias.value_type(db), relation, visitor) + }), + // Pretend that instances of `dataclasses.Field` are assignable to their default type. // This allows field definitions like `name: str = field(default="")` in dataclasses // to pass the assignability check of the inferred type to the declared type. @@ -1388,12 +1413,13 @@ impl<'db> Type<'db> { match bound_typevar.typevar(db).bound_or_constraints(db) { None => unreachable!(), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.has_relation_to(db, target, relation) + bound.has_relation_to_impl(db, target, relation, visitor) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + constraints.elements(db).iter().all(|constraint| { + constraint.has_relation_to_impl(db, target, relation, visitor) + }) } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.has_relation_to(db, target, relation)), } } @@ -1405,9 +1431,9 @@ impl<'db> Type<'db> { .typevar(db) .constraints(db) .is_some_and(|constraints| { - constraints - .iter() - .all(|constraint| self.has_relation_to(db, *constraint, relation)) + constraints.iter().all(|constraint| { + self.has_relation_to_impl(db, *constraint, relation, visitor) + }) }) => { true @@ -1421,12 +1447,12 @@ impl<'db> Type<'db> { (Type::Union(union), _) => union .elements(db) .iter() - .all(|&elem_ty| elem_ty.has_relation_to(db, target, relation)), + .all(|&elem_ty| elem_ty.has_relation_to_impl(db, target, relation, visitor)), (_, Type::Union(union)) => union .elements(db) .iter() - .any(|&elem_ty| self.has_relation_to(db, elem_ty, relation)), + .any(|&elem_ty| self.has_relation_to_impl(db, elem_ty, relation, visitor)), // 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, @@ -1435,7 +1461,7 @@ impl<'db> Type<'db> { intersection .positive(db) .iter() - .all(|&pos_ty| self.has_relation_to(db, pos_ty, relation)) + .all(|&pos_ty| self.has_relation_to_impl(db, pos_ty, relation, visitor)) && intersection .negative(db) .iter() @@ -1445,7 +1471,7 @@ impl<'db> Type<'db> { (Type::Intersection(intersection), _) => intersection .positive(db) .iter() - .any(|&elem_ty| elem_ty.has_relation_to(db, target, relation)), + .any(|&elem_ty| elem_ty.has_relation_to_impl(db, target, relation, visitor)), // 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. @@ -1503,9 +1529,9 @@ impl<'db> Type<'db> { self_callable.has_relation_to(db, other_callable, relation) } - (_, Type::Callable(_)) => self - .into_callable(db) - .is_some_and(|callable| callable.has_relation_to(db, target, relation)), + (_, Type::Callable(_)) => self.into_callable(db).is_some_and(|callable| { + callable.has_relation_to_impl(db, target, relation, visitor) + }), (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { left.has_relation_to(db, right, relation) @@ -1541,8 +1567,9 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(_) | Type::EnumLiteral(_), _, - ) => (self.literal_fallback_instance(db)) - .is_some_and(|instance| instance.has_relation_to(db, target, relation)), + ) => (self.literal_fallback_instance(db)).is_some_and(|instance| { + instance.has_relation_to_impl(db, target, relation, visitor) + }), // 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. @@ -1553,13 +1580,13 @@ impl<'db> Type<'db> { // The same reasoning applies for these special callable types: (Type::BoundMethod(_), _) => KnownClass::MethodType .to_instance(db) - .has_relation_to(db, target, relation), + .has_relation_to_impl(db, target, relation, visitor), (Type::MethodWrapper(_), _) => KnownClass::WrapperDescriptorType .to_instance(db) - .has_relation_to(db, target, relation), + .has_relation_to_impl(db, target, relation, visitor), (Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType .to_instance(db) - .has_relation_to(db, target, relation), + .has_relation_to_impl(db, target, relation, visitor), (Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => { // TODO: Implement subtyping using an equivalent `Callable` type. @@ -1568,24 +1595,30 @@ impl<'db> Type<'db> { // `TypeIs` is invariant. (Type::TypeIs(left), Type::TypeIs(right)) => { - left.return_type(db) - .has_relation_to(db, right.return_type(db), relation) - && right - .return_type(db) - .has_relation_to(db, left.return_type(db), relation) + left.return_type(db).has_relation_to_impl( + db, + right.return_type(db), + relation, + visitor, + ) && right.return_type(db).has_relation_to_impl( + db, + left.return_type(db), + relation, + visitor, + ) } // `TypeIs[T]` is a subtype of `bool`. (Type::TypeIs(_), _) => KnownClass::Bool .to_instance(db) - .has_relation_to(db, target, relation), + .has_relation_to_impl(db, target, relation, visitor), // 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) => + .has_relation_to_impl(db, target, relation, visitor) => { true } @@ -1595,7 +1628,7 @@ impl<'db> Type<'db> { (Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target), (Type::BoundSuper(_), _) => KnownClass::Super .to_instance(db) - .has_relation_to(db, target, relation), + .has_relation_to_impl(db, target, relation, visitor), // `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`. @@ -1603,20 +1636,30 @@ impl<'db> Type<'db> { .subclass_of() .into_class() .map(|subclass_of_class| { - ClassType::NonGeneric(class).has_relation_to(db, subclass_of_class, relation) + ClassType::NonGeneric(class).has_relation_to_impl( + db, + subclass_of_class, + relation, + visitor, + ) }) .unwrap_or(relation.is_assignability()), (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() .into_class() .map(|subclass_of_class| { - ClassType::Generic(alias).has_relation_to(db, subclass_of_class, relation) + ClassType::Generic(alias).has_relation_to_impl( + db, + subclass_of_class, + relation, + visitor, + ) }) .unwrap_or(relation.is_assignability()), // 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.has_relation_to(db, target_subclass_ty, relation) + self_subclass_ty.has_relation_to_impl(db, target_subclass_ty, relation, visitor) } // `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`. @@ -1624,25 +1667,30 @@ impl<'db> Type<'db> { // is an instance of its metaclass `abc.ABCMeta`. (Type::ClassLiteral(class), _) => class .metaclass_instance_type(db) - .has_relation_to(db, target, relation), + .has_relation_to_impl(db, target, relation, visitor), (Type::GenericAlias(alias), _) => ClassType::from(alias) .metaclass_instance_type(db) - .has_relation_to(db, target, relation), + .has_relation_to_impl(db, target, relation, visitor), // `type[Any]` is a subtype of `type[object]`, and is assignable to any `type[...]` (Type::SubclassOf(subclass_of_ty), other) if subclass_of_ty.is_dynamic() => { KnownClass::Type .to_instance(db) - .has_relation_to(db, other, relation) + .has_relation_to_impl(db, other, relation, visitor) || (relation.is_assignability() - && other.has_relation_to(db, KnownClass::Type.to_instance(db), relation)) + && other.has_relation_to_impl( + db, + KnownClass::Type.to_instance(db), + relation, + visitor, + )) } // Any `type[...]` type is assignable to `type[Any]` (other, Type::SubclassOf(subclass_of_ty)) if subclass_of_ty.is_dynamic() && relation.is_assignability() => { - other.has_relation_to(db, KnownClass::Type.to_instance(db), relation) + other.has_relation_to_impl(db, KnownClass::Type.to_instance(db), relation, visitor) } // `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses @@ -1657,31 +1705,36 @@ impl<'db> Type<'db> { .into_class() .map(|class| class.metaclass_instance_type(db)) .unwrap_or_else(|| KnownClass::Type.to_instance(db)) - .has_relation_to(db, target, relation), + .has_relation_to_impl(db, target, relation, visitor), // 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) - .has_relation_to(db, right, relation), + .has_relation_to_impl(db, right, relation, visitor), (Type::KnownInstance(left), right) => left .instance_fallback(db) - .has_relation_to(db, right, relation), + .has_relation_to_impl(db, right, relation, visitor), // `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.has_relation_to(db, target_instance, relation) + visitor.visit((self, target), || { + self_instance.has_relation_to_impl(db, target_instance, relation, visitor) + }) } (Type::PropertyInstance(_), _) => KnownClass::Property .to_instance(db) - .has_relation_to(db, target, relation), - (_, Type::PropertyInstance(_)) => { - self.has_relation_to(db, KnownClass::Property.to_instance(db), relation) - } + .has_relation_to_impl(db, target, relation, visitor), + (_, Type::PropertyInstance(_)) => self.has_relation_to_impl( + db, + KnownClass::Property.to_instance(db), + relation, + visitor, + ), // Other than the special cases enumerated above, `Instance` types and typevars are // never subtypes of any other variants @@ -1702,6 +1755,15 @@ impl<'db> Type<'db> { /// /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { + self.is_equivalent_to_impl(db, other, &PairVisitor::new(true)) + } + + pub(crate) fn is_equivalent_to_impl( + self, + db: &'db dyn Db, + other: Type<'db>, + visitor: &PairVisitor<'db>, + ) -> bool { if self == other { return true; } @@ -1717,6 +1779,20 @@ impl<'db> Type<'db> { } } + (Type::TypeAlias(self_alias), _) => { + let self_alias_ty = self_alias.value_type(db); + visitor.visit((self_alias_ty, other), || { + self_alias_ty.is_equivalent_to_impl(db, other, visitor) + }) + } + + (_, Type::TypeAlias(other_alias)) => { + let other_alias_ty = other_alias.value_type(db); + visitor.visit((self, other_alias_ty), || { + self.is_equivalent_to_impl(db, other_alias_ty, visitor) + }) + } + (Type::NominalInstance(first), Type::NominalInstance(second)) => { first.is_equivalent_to(db, second) } @@ -1801,6 +1877,20 @@ impl<'db> Type<'db> { false } + (Type::TypeAlias(alias), _) => { + let self_alias_ty = alias.value_type(db); + visitor.visit((self_alias_ty, other), || { + self_alias_ty.is_disjoint_from_impl(db, other, visitor) + }) + } + + (_, Type::TypeAlias(alias)) => { + let other_alias_ty = alias.value_type(db); + visitor.visit((self, other_alias_ty), || { + self.is_disjoint_from_impl(db, other_alias_ty, visitor) + }) + } + // A typevar is never disjoint from itself, since all occurrences of the typevar must // be specialized to the same type. (This is an important difference between typevars // and `Any`!) Different typevars might be disjoint, depending on their bounds and @@ -1944,14 +2034,15 @@ impl<'db> Type<'db> { } (Type::ProtocolInstance(protocol), Type::SpecialForm(special_form)) - | (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => { + | (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => visitor.visit((self, other), || { any_protocol_members_absent_or_disjoint(db, protocol, special_form.instance_fallback(db), visitor) - } + }), + (Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance)) - | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => { + | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => visitor.visit((self, other), || { any_protocol_members_absent_or_disjoint(db, protocol, known_instance.instance_fallback(db), visitor) - } + }), // The absence of a protocol member on one of these types guarantees // that the type will be disjoint from the protocol, @@ -2006,27 +2097,27 @@ impl<'db> Type<'db> { | Type::GenericAlias(..) | Type::IntLiteral(..) | Type::EnumLiteral(..)), - ) => any_protocol_members_absent_or_disjoint(db, protocol, ty, visitor), + ) => visitor.visit((self, other), || any_protocol_members_absent_or_disjoint(db, protocol, ty, visitor)), // This is the same as the branch above -- // once guard patterns are stabilised, it could be unified with that branch // () (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) - if n.class(db).is_final(db) => + if n.class(db).is_final(db) => visitor.visit((self, other), || { any_protocol_members_absent_or_disjoint(db, protocol, nominal, visitor) - } + }), (Type::ProtocolInstance(protocol), other) - | (other, Type::ProtocolInstance(protocol)) => { + | (other, Type::ProtocolInstance(protocol)) => visitor.visit((self, other), || { protocol.interface(db).members(db).any(|member| { matches!( other.member(db, member.name()).place, Place::Type(attribute_type, _) if member.has_disjoint_type_from(db, attribute_type, visitor) ) }) - } + }), (Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b)) | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { @@ -2349,6 +2440,7 @@ impl<'db> Type<'db> { Type::AlwaysTruthy | Type::AlwaysFalsy => false, Type::TypeIs(type_is) => type_is.is_bound(db), Type::TypedDict(_) => false, + Type::TypeAlias(alias) => alias.value_type(db).is_singleton(db), } } @@ -2428,6 +2520,8 @@ impl<'db> Type<'db> { Type::TypeIs(type_is) => type_is.is_bound(db), + Type::TypeAlias(alias) => alias.value_type(db).is_single_valued(db), + Type::Dynamic(_) | Type::Never | Type::Union(..) @@ -2546,6 +2640,10 @@ impl<'db> Type<'db> { } } + Type::TypeAlias(alias) => alias + .value_type(db) + .find_name_in_mro_with_policy(db, name, policy), + Type::FunctionLiteral(_) | Type::Callable(_) | Type::BoundMethod(_) @@ -2726,6 +2824,8 @@ impl<'db> Type<'db> { } Type::TypedDict(_) => Place::Unbound.into(), + + Type::TypeAlias(alias) => alias.value_type(db).instance_member(db, name), } } @@ -3199,6 +3299,10 @@ impl<'db> Type<'db> { policy, ), + Type::TypeAlias(alias) => alias + .value_type(db) + .member_lookup_with_policy(db, name, policy), + Type::NominalInstance(..) | Type::ProtocolInstance(..) | Type::BooleanLiteral(..) @@ -3580,6 +3684,9 @@ impl<'db> Type<'db> { Type::BooleanLiteral(bool) => Truthiness::from(*bool), Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()), Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()), + Type::TypeAlias(alias) => alias + .value_type(db) + .try_bool_impl(db, allow_short_circuit)?, }; Ok(truthiness) @@ -4537,6 +4644,8 @@ impl<'db> Type<'db> { known_instance.instance_fallback(db).bindings(db) } + Type::TypeAlias(alias) => alias.value_type(db).bindings(db), + Type::PropertyInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy @@ -5172,6 +5281,7 @@ impl<'db> Type<'db> { bound_typevar.binding_context(db), ))) } + Type::TypeAlias(alias) => alias.value_type(db).to_instance(db), Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance")), Type::BooleanLiteral(_) | Type::BytesLiteral(_) @@ -5275,7 +5385,7 @@ impl<'db> Type<'db> { }), Type::KnownInstance(known_instance) => match known_instance { - KnownInstanceType::TypeAliasType(alias) => Ok(alias.value_type(db)), + KnownInstanceType::TypeAliasType(alias) => Ok(Type::TypeAlias(*alias)), KnownInstanceType::TypeVar(typevar) => { let module = parsed_module(db, scope_id.file(db)).load(db); let index = semantic_index(db, scope_id.file(db)); @@ -5494,6 +5604,12 @@ impl<'db> Type<'db> { }, Type::Intersection(_) => Ok(todo_type!("Type::Intersection.in_type_expression")), + + Type::TypeAlias(alias) => { + alias + .value_type(db) + .in_type_expression(db, scope_id, typevar_binding_context) + } } } @@ -5565,6 +5681,7 @@ impl<'db> Type<'db> { Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class), + Type::TypeAlias(alias) => alias.value_type(db).to_meta_type(db), } } @@ -5618,6 +5735,15 @@ impl<'db> Type<'db> { self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + ) -> Type<'db> { + self.apply_type_mapping_impl(db, type_mapping, &TypeTransformer::default()) + } + + fn apply_type_mapping_impl<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> Type<'db> { match self { Type::TypeVar(bound_typevar) => match type_mapping { @@ -5650,10 +5776,10 @@ impl<'db> Type<'db> { )), Type::NominalInstance(instance) => - instance.apply_type_mapping(db, type_mapping), + instance.apply_type_mapping_impl(db, type_mapping, visitor), Type::ProtocolInstance(instance) => { - Type::ProtocolInstance(instance.apply_type_mapping(db, type_mapping)) + Type::ProtocolInstance(instance.apply_type_mapping_impl(db, type_mapping, visitor)) } Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { @@ -5685,15 +5811,15 @@ impl<'db> Type<'db> { } Type::GenericAlias(generic) => { - Type::GenericAlias(generic.apply_type_mapping(db, type_mapping)) + Type::GenericAlias(generic.apply_type_mapping_impl(db, type_mapping, visitor)) } Type::TypedDict(typed_dict) => { - Type::TypedDict(typed_dict.apply_type_mapping(db, type_mapping)) + Type::TypedDict(typed_dict.apply_type_mapping_impl(db, type_mapping, visitor)) } Type::SubclassOf(subclass_of) => Type::SubclassOf( - subclass_of.apply_type_mapping(db, type_mapping), + subclass_of.apply_type_mapping_impl(db, type_mapping, visitor), ), Type::PropertyInstance(property) => { @@ -5701,23 +5827,27 @@ impl<'db> Type<'db> { } Type::Union(union) => union.map(db, |element| { - element.apply_type_mapping(db, type_mapping) + element.apply_type_mapping_impl(db, type_mapping, visitor) }), Type::Intersection(intersection) => { let mut builder = IntersectionBuilder::new(db); for positive in intersection.positive(db) { builder = - builder.add_positive(positive.apply_type_mapping(db, type_mapping)); + builder.add_positive(positive.apply_type_mapping_impl(db, type_mapping, visitor)); } for negative in intersection.negative(db) { builder = - builder.add_negative(negative.apply_type_mapping(db, type_mapping)); + builder.add_negative(negative.apply_type_mapping_impl(db, type_mapping, visitor)); } builder.build() } Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping)), + Type::TypeAlias(alias) => { + visitor.visit(self, || alias.value_type(db).apply_type_mapping_impl(db, type_mapping, visitor)) + } + Type::ModuleLiteral(_) | Type::IntLiteral(_) | Type::BooleanLiteral(_) @@ -5842,6 +5972,12 @@ impl<'db> Type<'db> { .find_legacy_typevars(db, binding_context, typevars); } + Type::TypeAlias(alias) => { + alias + .value_type(db) + .find_legacy_typevars(db, binding_context, typevars); + } + Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy @@ -5954,6 +6090,8 @@ impl<'db> Type<'db> { SubclassOfInner::Dynamic(_) => None, }, + Self::TypeAlias(alias) => alias.value_type(db).definition(db), + Self::StringLiteral(_) | Self::BooleanLiteral(_) | Self::LiteralString @@ -8412,7 +8550,7 @@ impl<'db> PEP695TypeAliasType<'db> { semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) } - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_fn=value_type_cycle_recover, cycle_initial=value_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { let scope = self.rhs_scope(db); let module = parsed_module(db, scope.file(db)).load(db); @@ -8426,6 +8564,19 @@ impl<'db> PEP695TypeAliasType<'db> { } } +fn value_type_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Type<'db>, + _count: u32, + _self: PEP695TypeAliasType<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn value_type_cycle_initial<'db>(_db: &'db dyn Db, _self: PEP695TypeAliasType<'db>) -> Type<'db> { + Type::Never +} + /// # Ordering /// Ordering is based on the type alias's salsa-assigned id and not on its values. /// The id may change between runs, or when the alias was garbage collected and recreated. @@ -8464,7 +8615,9 @@ impl<'db> BareTypeAliasType<'db> { Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update, get_size2::GetSize, )] pub enum TypeAliasType<'db> { + /// A type alias defined using the PEP 695 `type` statement. PEP695(PEP695TypeAliasType<'db>), + /// A type alias defined by manually instantiating the PEP 695 `types.TypeAliasType`. Bare(BareTypeAliasType<'db>), } @@ -8547,7 +8700,7 @@ impl get_size2::GetSize for UnionType<'_> {} impl<'db> UnionType<'db> { /// Create a union from a list of elements /// (which may be eagerly simplified into a different variant of [`Type`] altogether). - pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + pub(crate) fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> where I: IntoIterator, T: Into>, @@ -9037,13 +9190,16 @@ impl<'db> TypedDictType<'db> { class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict) } - pub(crate) fn apply_type_mapping<'a>( + pub(crate) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> Self { Self { - defining_class: self.defining_class.apply_type_mapping(db, type_mapping), + defining_class: self + .defining_class + .apply_type_mapping_impl(db, type_mapping, visitor), } } } diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 7ad2215c96..8e529e4fca 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -246,6 +246,7 @@ impl<'db> UnionBuilder<'db> { } // Adding `Never` to a union is a no-op. Type::Never => {} + Type::TypeAlias(alias) => self.add_in_place(alias.value_type(self.db)), // If adding a string literal, look for an existing `UnionElement::StringLiterals` to // add it to, or an existing element that is a super-type of string literals, which // means we shouldn't add it. Otherwise, add a new `UnionElement::StringLiterals` @@ -542,6 +543,10 @@ impl<'db> IntersectionBuilder<'db> { pub(crate) fn add_positive(mut self, ty: Type<'db>) -> Self { match ty { + Type::TypeAlias(alias) => { + let value_type = alias.value_type(self.db); + self.add_positive(value_type) + } Type::Union(union) => { // Distribute ourself over this union: for each union element, clone ourself and // intersect with that union element, then create a new union-of-intersections with all @@ -629,6 +634,10 @@ impl<'db> IntersectionBuilder<'db> { // See comments above in `add_positive`; this is just the negated version. match ty { + Type::TypeAlias(alias) => { + let value_type = alias.value_type(self.db); + self.add_negative(value_type) + } Type::Union(union) => { for elem in union.elements(self.db) { self = self.add_negative(*elem); diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 017c9bdcd7..a282dd7a28 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -43,7 +43,7 @@ use crate::{ }, types::{ CallArguments, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType, - definition_expression_type, + cyclic::PairVisitor, definition_expression_type, }, }; use indexmap::IndexSet; @@ -251,15 +251,17 @@ impl<'db> GenericAlias<'db> { self.origin(db).definition(db) } - pub(super) fn apply_type_mapping<'a>( + pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> Self { Self::new( db, self.origin(db), - self.specialization(db).apply_type_mapping(db, type_mapping), + self.specialization(db) + .apply_type_mapping_impl(db, type_mapping, visitor), ) } @@ -400,14 +402,17 @@ impl<'db> ClassType<'db> { self.is_known(db, KnownClass::Object) } - pub(super) fn apply_type_mapping<'a>( + pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> Self { match self { Self::NonGeneric(_) => self, - Self::Generic(generic) => Self::Generic(generic.apply_type_mapping(db, type_mapping)), + Self::Generic(generic) => { + Self::Generic(generic.apply_type_mapping_impl(db, type_mapping, visitor)) + } } } @@ -456,14 +461,15 @@ impl<'db> ClassType<'db> { /// 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) + self.has_relation_to_impl(db, other, TypeRelation::Subtyping, &PairVisitor::new(true)) } - pub(super) fn has_relation_to( + pub(super) fn has_relation_to_impl( self, db: &'db dyn Db, other: Self, relation: TypeRelation, + visitor: &PairVisitor<'db>, ) -> bool { self.iter_mro(db).any(|base| { match base { @@ -479,10 +485,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).has_relation_to( + && base.specialization(db).has_relation_to_impl( db, other.specialization(db), relation, + visitor, ) } (ClassType::Generic(_), ClassType::NonGeneric(_)) diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index d381c94110..bd1c0ad598 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -135,6 +135,8 @@ impl<'db> ClassBase<'db> { // in which case we want to treat `Never` in a forgiving way and silence diagnostics Type::Never => Some(ClassBase::unknown()), + Type::TypeAlias(alias) => Self::try_from_type(db, alias.value_type(db)), + Type::PropertyInstance(_) | Type::BooleanLiteral(_) | Type::FunctionLiteral(_) @@ -247,9 +249,16 @@ impl<'db> ClassBase<'db> { } } - fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + fn apply_type_mapping_impl<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, + ) -> Self { match self { - Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)), + Self::Class(class) => { + Self::Class(class.apply_type_mapping_impl(db, type_mapping, visitor)) + } Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => self, } } @@ -260,7 +269,11 @@ impl<'db> ClassBase<'db> { specialization: Option>, ) -> Self { if let Some(specialization) = specialization { - self.apply_type_mapping(db, &TypeMapping::Specialization(specialization)) + self.apply_type_mapping_impl( + db, + &TypeMapping::Specialization(specialization), + &TypeTransformer::default(), + ) } else { self } diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index b5e36a3875..228449a916 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -1,3 +1,23 @@ +//! Cycle detection for recursive types. +//! +//! The visitors here (`TypeTransformer` and `PairVisitor`) are used in methods that recursively +//! visit types to transform them (e.g. `Type::normalize`) or to decide a relation between a pair +//! of types (e.g. `Type::has_relation_to`). +//! +//! The typical pattern is that the "entry" method (e.g. `Type::has_relation_to`) will create a +//! visitor and pass it to the recursive method (e.g. `Type::has_relation_to_impl`). Rust types +//! that form part of a complex type (e.g. tuples, protocols, nominal instances, etc) should +//! usually just implement the recursive method, and all recursive calls should call the recursive +//! method and pass along the visitor. +//! +//! Not all recursive calls need to actually call `.visit` on the visitor; only when visiting types +//! that can create a recursive relationship (this includes, for example, type aliases and +//! protocols). +//! +//! There is a risk of double-visiting, for example if `Type::has_relation_to_impl` calls +//! `visitor.visit` when visiting a protocol type, and then internal `has_relation_to_impl` methods +//! of the Rust types implementing protocols also call `visitor.visit`. The best way to avoid this +//! is to prefer always calling `visitor.visit` only in the main recursive method on `Type`. use rustc_hash::FxHashMap; use crate::FxIndexSet; @@ -22,17 +42,17 @@ pub(crate) type PairVisitor<'db> = CycleDetector<(Type<'db>, Type<'db>), bool>; #[derive(Debug)] pub(crate) struct CycleDetector { - /// If the type we're visiting is present in `seen`, - /// it indicates that we've hit a cycle (due to a recursive type); - /// we need to immediately short circuit the whole operation and return the fallback value. - /// That's why we pop items off the end of `seen` after we've visited them. + /// If the type we're visiting is present in `seen`, it indicates that we've hit a cycle (due + /// to a recursive type); we need to immediately short circuit the whole operation and return + /// the fallback value. That's why we pop items off the end of `seen` after we've visited them. seen: RefCell>, - /// Unlike `seen`, this field is a pure performance optimisation (and an essential one). - /// If the type we're trying to normalize is present in `cache`, it doesn't necessarily mean we've hit a cycle: - /// it just means that we've already visited this inner type as part of a bigger call chain we're currently in. - /// Since this cache is just a performance optimisation, it doesn't make sense to pop items off the end of the - /// cache after they've been visited (it would sort-of defeat the point of a cache if we did!) + /// Unlike `seen`, this field is a pure performance optimisation (and an essential one). If the + /// type we're trying to normalize is present in `cache`, it doesn't necessarily mean we've hit + /// a cycle: it just means that we've already visited this inner type as part of a bigger call + /// chain we're currently in. Since this cache is just a performance optimisation, it doesn't + /// make sense to pop items off the end of the cache after they've been visited (it would + /// sort-of defeat the point of a cache if we did!) cache: RefCell>, fallback: R, @@ -48,8 +68,8 @@ impl CycleDetector { } pub(crate) fn visit(&self, item: T, func: impl FnOnce() -> R) -> R { - if let Some(ty) = self.cache.borrow().get(&item) { - return *ty; + if let Some(val) = self.cache.borrow().get(&item) { + return *val; } // We hit a cycle diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 76cea209f5..db8c67f8f3 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -243,6 +243,7 @@ impl Display for DisplayRepresentation<'_> { f.write_str("]") } Type::TypedDict(typed_dict) => f.write_str(typed_dict.defining_class.name(self.db)), + Type::TypeAlias(alias) => f.write_str(alias.name(self.db)), } } } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 17ab1342f6..cbc6e1274a 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1000,6 +1000,8 @@ fn is_instance_truthiness<'db>( Type::ClassLiteral(..) => always_true_if(is_instance(&KnownClass::Type.to_instance(db))), + Type::TypeAlias(alias) => is_instance_truthiness(db, alias.value_type(db), class), + Type::BoundMethod(..) | Type::MethodWrapper(..) | Type::WrapperDescriptor(..) diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index d4610a4cb2..2026f62cad 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -16,7 +16,7 @@ use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::{ BoundTypeVarInstance, KnownClass, KnownInstanceType, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType, - binding_type, declaration_type, + binding_type, cyclic::PairVisitor, declaration_type, }; use crate::{Db, FxOrderSet}; @@ -304,9 +304,9 @@ impl<'db> GenericContext<'db> { pub(crate) fn specialize_tuple( self, db: &'db dyn Db, + element_type: Type<'db>, tuple: TupleType<'db>, ) -> Specialization<'db> { - let element_type = tuple.tuple(db).homogeneous_element_type(db); Specialization::new(db, self, Box::from([element_type]), Some(tuple)) } @@ -463,15 +463,24 @@ impl<'db> Specialization<'db> { self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + self.apply_type_mapping_impl(db, type_mapping, &TypeTransformer::default()) + } + + pub(crate) fn apply_type_mapping_impl<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> Self { let types: Box<[_]> = self .types(db) .iter() - .map(|ty| ty.apply_type_mapping(db, type_mapping)) + .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)) .collect(); let tuple_inner = self .tuple_inner(db) - .and_then(|tuple| tuple.apply_type_mapping(db, type_mapping)); + .and_then(|tuple| tuple.apply_type_mapping_impl(db, type_mapping, visitor)); Specialization::new(db, self.generic_context(db), types, tuple_inner) } @@ -549,11 +558,12 @@ impl<'db> Specialization<'db> { Specialization::new(db, self.generic_context(db), types, tuple_inner) } - pub(crate) fn has_relation_to( + pub(crate) fn has_relation_to_impl( self, db: &'db dyn Db, other: Self, relation: TypeRelation, + visitor: &PairVisitor<'db>, ) -> bool { let generic_context = self.generic_context(db); if generic_context != other.generic_context(db) { @@ -562,7 +572,7 @@ impl<'db> Specialization<'db> { if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db)) { - return self_tuple.has_relation_to(db, other_tuple, relation); + return self_tuple.has_relation_to_impl(db, other_tuple, relation, visitor); } for ((bound_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) @@ -590,9 +600,11 @@ impl<'db> Specialization<'db> { && other_type.is_assignable_to(db, *self_type) } }, - TypeVarVariance::Covariant => self_type.has_relation_to(db, *other_type, relation), + TypeVarVariance::Covariant => { + self_type.has_relation_to_impl(db, *other_type, relation, visitor) + } TypeVarVariance::Contravariant => { - other_type.has_relation_to(db, *self_type, relation) + other_type.has_relation_to_impl(db, *self_type, relation, visitor) } TypeVarVariance::Bivariant => true, }; diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index ceee7ac0ee..39262c8b33 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -132,6 +132,8 @@ impl<'db> AllMembers<'db> { Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy | Type::AlwaysFalsy => {} + Type::TypeAlias(alias) => self.extend_with_type(db, alias.value_type(db)), + Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::StringLiteral(_) diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index a93b730321..539b82762b 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -3956,6 +3956,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + Type::TypeAlias(alias) => self.validate_attribute_assignment( + target, + alias.value_type(self.db()), + attribute, + value_ty, + emit_diagnostics, + ), + // Super instances do not allow attribute assignment Type::NominalInstance(instance) if instance.class(db).is_known(db, KnownClass::Super) => @@ -7143,10 +7151,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let operand_type = self.infer_expression(operand); + self.infer_unary_expression_type(*op, operand_type, unary) + } + + fn infer_unary_expression_type( + &mut self, + op: ast::UnaryOp, + operand_type: Type<'db>, + unary: &ast::ExprUnaryOp, + ) -> Type<'db> { match (op, operand_type) { (_, Type::Dynamic(_)) => operand_type, (_, Type::Never) => Type::Never, + (_, Type::TypeAlias(alias)) => { + self.infer_unary_expression_type(op, alias.value_type(self.db()), unary) + } + (ast::UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value), (ast::UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value), (ast::UnaryOp::Invert, Type::IntLiteral(value)) => Type::IntLiteral(!value), @@ -7307,6 +7328,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) }), + (Type::TypeAlias(alias), rhs, _) => self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + alias.value_type(self.db()), + rhs, + op, + ), + + (lhs, Type::TypeAlias(alias), _) => self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + lhs, + alias.value_type(self.db()), + op, + ), + // Non-todo Anys take precedence over Todos (as if we fix this `Todo` in the future, // the result would then become Any or Unknown, respectively). (any @ Type::Dynamic(DynamicType::Any), _, _) diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 8b2967e744..31b2a1f9e2 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -260,20 +260,21 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn has_relation_to( + pub(super) fn has_relation_to_impl( self, db: &'db dyn Db, other: Self, relation: TypeRelation, + visitor: &PairVisitor<'db>, ) -> bool { match (self.0, other.0) { ( NominalInstanceInner::ExactTuple(tuple1), NominalInstanceInner::ExactTuple(tuple2), - ) => tuple1.has_relation_to(db, tuple2, relation), + ) => tuple1.has_relation_to_impl(db, tuple2, relation, visitor), _ => self .class(db) - .has_relation_to(db, other.class(db), relation), + .has_relation_to_impl(db, other.class(db), relation, visitor), } } @@ -340,17 +341,18 @@ impl<'db> NominalInstanceType<'db> { SubclassOfType::from(db, self.class(db)) } - pub(super) fn apply_type_mapping<'a>( + pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> Type<'db> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => { - Type::tuple(tuple.apply_type_mapping(db, type_mapping)) + Type::tuple(tuple.apply_type_mapping_impl(db, type_mapping, visitor)) } NominalInstanceInner::NonTuple(class) => { - Type::non_tuple_instance(class.apply_type_mapping(db, type_mapping)) + Type::non_tuple_instance(class.apply_type_mapping_impl(db, type_mapping, visitor)) } } } @@ -552,17 +554,18 @@ impl<'db> ProtocolInstanceType<'db> { } } - pub(super) fn apply_type_mapping<'a>( + pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> Self { match self.inner { Protocol::FromClass(class) => { - Self::from_class(class.apply_type_mapping(db, type_mapping)) + Self::from_class(class.apply_type_mapping_impl(db, type_mapping, visitor)) } Protocol::Synthesized(synthesized) => { - Self::synthesized(synthesized.apply_type_mapping(db, type_mapping)) + Self::synthesized(synthesized.apply_type_mapping_impl(db, type_mapping, visitor)) } } } @@ -646,10 +649,11 @@ mod synthesized_protocol { Self(self.0.materialize(db, variance)) } - pub(super) fn apply_type_mapping<'a>( + pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + _visitor: &TypeTransformer<'db>, ) -> Self { Self(self.0.specialized_and_normalized(db, type_mapping)) } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 3f02677f41..2693570e1c 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -182,6 +182,7 @@ impl ClassInfoConstraintFunction { }; match classinfo { + Type::TypeAlias(alias) => self.generate_constraint(db, alias.value_type(db)), Type::ClassLiteral(class_literal) => { // At runtime (on Python 3.11+), this will return `True` for classes that actually // do inherit `typing.Any` and `False` otherwise. We could accurately model that? diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index bdcba1c5d6..eab39163fd 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -437,9 +437,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { match &self.kind { // TODO: implement disjointness for property/method members as well as attribute members ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => false, - ProtocolMemberKind::Other(ty) => visitor.visit((*ty, other), || { - ty.is_disjoint_from_impl(db, other, visitor) - }), + ProtocolMemberKind::Other(ty) => ty.is_disjoint_from_impl(db, other, visitor), } } diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index fdedce211f..8840bdb025 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -4,7 +4,7 @@ use crate::place::PlaceAndQualifiers; use crate::semantic_index::definition::Definition; use crate::types::{ BindingContext, BoundTypeVarInstance, ClassType, DynamicType, KnownClass, MemberLookupPolicy, - Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, + Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, cyclic::PairVisitor, }; use crate::{Db, FxOrderSet}; @@ -112,14 +112,19 @@ impl<'db> SubclassOfType<'db> { } } - pub(super) fn apply_type_mapping<'a>( + pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> Self { match self.subclass_of { SubclassOfInner::Class(class) => Self { - subclass_of: SubclassOfInner::Class(class.apply_type_mapping(db, type_mapping)), + subclass_of: SubclassOfInner::Class(class.apply_type_mapping_impl( + db, + type_mapping, + visitor, + )), }, SubclassOfInner::Dynamic(_) => self, } @@ -149,11 +154,12 @@ impl<'db> SubclassOfType<'db> { } /// Return `true` if `self` has a certain relation to `other`. - pub(crate) fn has_relation_to( + pub(crate) fn has_relation_to_impl( self, db: &'db dyn Db, other: SubclassOfType<'db>, relation: TypeRelation, + visitor: &PairVisitor<'db>, ) -> bool { match (self.subclass_of, other.subclass_of) { (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => { @@ -168,7 +174,7 @@ impl<'db> SubclassOfType<'db> { // 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)) => { - self_class.has_relation_to(db, other_class, relation) + self_class.has_relation_to_impl(db, other_class, relation, visitor) } } } diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 230040ea80..c4f5943240 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -223,7 +223,7 @@ impl<'db> TupleType<'db> { // N.B. If this method is not Salsa-tracked, we take 10 minutes to check // `static-frame` as part of a mypy_primer run! This is because it's called // from `NominalInstanceType::class()`, which is a very hot method. - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_fn=to_class_type_cycle_recover, cycle_initial=to_class_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn to_class_type(self, db: &'db dyn Db) -> ClassType<'db> { let tuple_class = KnownClass::Tuple .try_to_class_literal(db) @@ -231,7 +231,8 @@ impl<'db> TupleType<'db> { tuple_class.apply_specialization(db, |generic_context| { if generic_context.variables(db).len() == 1 { - generic_context.specialize_tuple(db, self) + let element_type = self.tuple(db).homogeneous_element_type(db); + generic_context.specialize_tuple(db, element_type, self) } else { generic_context.default_specialization(db, Some(KnownClass::Tuple)) } @@ -254,12 +255,17 @@ impl<'db> TupleType<'db> { TupleType::new(db, self.tuple(db).materialize(db, variance)) } - pub(crate) fn apply_type_mapping<'a>( + pub(crate) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> Option { - TupleType::new(db, self.tuple(db).apply_type_mapping(db, type_mapping)) + TupleType::new( + db, + self.tuple(db) + .apply_type_mapping_impl(db, type_mapping, visitor), + ) } pub(crate) fn find_legacy_typevars( @@ -272,14 +278,15 @@ impl<'db> TupleType<'db> { .find_legacy_typevars(db, binding_context, typevars); } - pub(crate) fn has_relation_to( + pub(crate) fn has_relation_to_impl( self, db: &'db dyn Db, other: Self, relation: TypeRelation, + visitor: &PairVisitor<'db>, ) -> bool { self.tuple(db) - .has_relation_to(db, other.tuple(db), relation) + .has_relation_to_impl(db, other.tuple(db), relation, visitor) } pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { @@ -291,6 +298,29 @@ impl<'db> TupleType<'db> { } } +fn to_class_type_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &ClassType<'db>, + _count: u32, + _self: TupleType<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn to_class_type_cycle_initial<'db>(db: &'db dyn Db, self_: TupleType<'db>) -> ClassType<'db> { + let tuple_class = KnownClass::Tuple + .try_to_class_literal(db) + .expect("Typeshed should always have a `tuple` class in `builtins.pyi`"); + + tuple_class.apply_specialization(db, |generic_context| { + if generic_context.variables(db).len() == 1 { + generic_context.specialize_tuple(db, Type::Never, self_) + } else { + generic_context.default_specialization(db, Some(KnownClass::Tuple)) + } + }) +} + /// A tuple spec describes the contents of a tuple type, which might be fixed- or variable-length. /// /// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a @@ -379,11 +409,16 @@ impl<'db> FixedLengthTuple> { Self::from_elements(self.0.iter().map(|ty| ty.materialize(db, variance))) } - fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + fn apply_type_mapping_impl<'a>( + &self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, + ) -> Self { Self::from_elements( self.0 .iter() - .map(|ty| ty.apply_type_mapping(db, type_mapping)), + .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)), ) } @@ -398,18 +433,19 @@ impl<'db> FixedLengthTuple> { } } - fn has_relation_to( + fn has_relation_to_impl( &self, db: &'db dyn Db, other: &Tuple>, relation: TypeRelation, + visitor: &PairVisitor<'db>, ) -> bool { match other { Tuple::Fixed(other) => { self.0.len() == other.0.len() - && (self.0.iter()) - .zip(&other.0) - .all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation)) + && (self.0.iter()).zip(&other.0).all(|(self_ty, other_ty)| { + self_ty.has_relation_to_impl(db, *other_ty, relation, visitor) + }) } Tuple::Variable(other) => { @@ -420,7 +456,7 @@ impl<'db> FixedLengthTuple> { let Some(self_ty) = self_iter.next() else { return false; }; - if !self_ty.has_relation_to(db, *other_ty, relation) { + if !self_ty.has_relation_to_impl(db, *other_ty, relation, visitor) { return false; } } @@ -428,14 +464,16 @@ impl<'db> FixedLengthTuple> { let Some(self_ty) = self_iter.next_back() else { return false; }; - if !self_ty.has_relation_to(db, *other_ty, relation) { + if !self_ty.has_relation_to_impl(db, *other_ty, relation, visitor) { return false; } } // In addition, any remaining elements in this tuple must satisfy the // variable-length portion of the other tuple. - self_iter.all(|self_ty| self_ty.has_relation_to(db, other.variable, relation)) + self_iter.all(|self_ty| { + self_ty.has_relation_to_impl(db, other.variable, relation, visitor) + }) } } } @@ -669,19 +707,21 @@ impl<'db> VariableLengthTuple> { ) } - fn apply_type_mapping<'a>( + fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> TupleSpec<'db> { Self::mixed( self.prefix .iter() - .map(|ty| ty.apply_type_mapping(db, type_mapping)), - self.variable.apply_type_mapping(db, type_mapping), + .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)), + self.variable + .apply_type_mapping_impl(db, type_mapping, visitor), self.suffix .iter() - .map(|ty| ty.apply_type_mapping(db, type_mapping)), + .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)), ) } @@ -701,11 +741,12 @@ impl<'db> VariableLengthTuple> { } } - fn has_relation_to( + fn has_relation_to_impl( &self, db: &'db dyn Db, other: &Tuple>, relation: TypeRelation, + visitor: &PairVisitor<'db>, ) -> bool { match other { Tuple::Fixed(other) => { @@ -732,7 +773,7 @@ impl<'db> VariableLengthTuple> { let Some(other_ty) = other_iter.next() else { return false; }; - if !self_ty.has_relation_to(db, other_ty, relation) { + if !self_ty.has_relation_to_impl(db, other_ty, relation, visitor) { return false; } } @@ -741,7 +782,7 @@ impl<'db> VariableLengthTuple> { let Some(other_ty) = other_iter.next_back() else { return false; }; - if !self_ty.has_relation_to(db, other_ty, relation) { + if !self_ty.has_relation_to_impl(db, other_ty, relation, visitor) { return false; } } @@ -771,10 +812,10 @@ impl<'db> VariableLengthTuple> { ) .all(|pair| match pair { EitherOrBoth::Both(self_ty, other_ty) => { - self_ty.has_relation_to(db, other_ty, relation) + self_ty.has_relation_to_impl(db, other_ty, relation, visitor) } EitherOrBoth::Left(self_ty) => { - self_ty.has_relation_to(db, other.variable, relation) + self_ty.has_relation_to_impl(db, other.variable, relation, visitor) } EitherOrBoth::Right(_) => { // The rhs has a required element that the lhs is not guaranteed to @@ -796,10 +837,10 @@ impl<'db> VariableLengthTuple> { .zip_longest(other_suffix.iter().rev()) .all(|pair| match pair { EitherOrBoth::Both(self_ty, other_ty) => { - self_ty.has_relation_to(db, *other_ty, relation) + self_ty.has_relation_to_impl(db, *other_ty, relation, visitor) } EitherOrBoth::Left(self_ty) => { - self_ty.has_relation_to(db, other.variable, relation) + self_ty.has_relation_to_impl(db, other.variable, relation, visitor) } EitherOrBoth::Right(_) => { // The rhs has a required element that the lhs is not guaranteed to @@ -812,7 +853,8 @@ impl<'db> VariableLengthTuple> { } // And lastly, the variable-length portions must satisfy the relation. - self.variable.has_relation_to(db, other.variable, relation) + self.variable + .has_relation_to_impl(db, other.variable, relation, visitor) } } } @@ -979,14 +1021,17 @@ impl<'db> Tuple> { } } - pub(crate) fn apply_type_mapping<'a>( + pub(crate) fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, + visitor: &TypeTransformer<'db>, ) -> Self { match self { - Tuple::Fixed(tuple) => Tuple::Fixed(tuple.apply_type_mapping(db, type_mapping)), - Tuple::Variable(tuple) => tuple.apply_type_mapping(db, type_mapping), + Tuple::Fixed(tuple) => { + Tuple::Fixed(tuple.apply_type_mapping_impl(db, type_mapping, visitor)) + } + Tuple::Variable(tuple) => tuple.apply_type_mapping_impl(db, type_mapping, visitor), } } @@ -1002,10 +1047,20 @@ impl<'db> Tuple> { } } - fn has_relation_to(&self, db: &'db dyn Db, other: &Self, relation: TypeRelation) -> bool { + fn has_relation_to_impl( + &self, + db: &'db dyn Db, + other: &Self, + relation: TypeRelation, + visitor: &PairVisitor<'db>, + ) -> bool { match self { - Tuple::Fixed(self_tuple) => self_tuple.has_relation_to(db, other, relation), - Tuple::Variable(self_tuple) => self_tuple.has_relation_to(db, other, relation), + Tuple::Fixed(self_tuple) => { + self_tuple.has_relation_to_impl(db, other, relation, visitor) + } + Tuple::Variable(self_tuple) => { + self_tuple.has_relation_to_impl(db, other, relation, visitor) + } } } diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index dd8fede9b5..0747fc8d17 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -204,6 +204,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::Dynamic(_), _) => Ordering::Less, (_, Type::Dynamic(_)) => Ordering::Greater, + (Type::TypeAlias(left), Type::TypeAlias(right)) => left.cmp(right), + (Type::TypeAlias(_), _) => Ordering::Less, + (_, Type::TypeAlias(_)) => Ordering::Greater, + (Type::Union(_), _) | (_, Type::Union(_)) => { unreachable!("our type representation does not permit nested unions"); } diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index 2bfd3b92bd..deb68eab74 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -117,6 +117,7 @@ enum NonAtomicType<'db> { TypeVar(BoundTypeVarInstance<'db>), ProtocolInstance(ProtocolInstanceType<'db>), TypedDict(TypedDictType<'db>), + TypeAlias(TypeAliasType<'db>), } enum TypeKind<'db> { @@ -183,6 +184,7 @@ impl<'db> From> for TypeKind<'db> { Type::TypedDict(typed_dict) => { TypeKind::NonAtomic(NonAtomicType::TypedDict(typed_dict)) } + Type::TypeAlias(alias) => TypeKind::NonAtomic(NonAtomicType::TypeAlias(alias)), } } } @@ -221,6 +223,9 @@ fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( visitor.visit_protocol_instance_type(db, protocol); } NonAtomicType::TypedDict(typed_dict) => visitor.visit_typed_dict_type(db, typed_dict), + NonAtomicType::TypeAlias(alias) => { + visitor.visit_type_alias_type(db, alias); + } } }