[ty] support recursive type aliases (#19805)

## Summary

Support recursive type aliases by adding a `Type::TypeAlias` type
variant, which allows referring to a type alias directly as a type
without eagerly unpacking it to its value.

We still unpack type aliases when they are added to intersections and
unions, so that we can simplify the intersection/union appropriately
based on the unpacked value of the type alias.

This introduces new possible recursive types, and so also requires
expanding our usage of recursion-detecting visitors in Type methods. The
use of these visitors is still not fully comprehensive in this PR, and
will require further expansion to support recursion in more kinds of
types (I already have further work on this locally), but I think it may
be better to do this incrementally in multiple PRs.

## Test Plan

Added some recursive type-alias tests and made them pass.
This commit is contained in:
Carl Meyer 2025-08-12 09:03:10 -07:00 committed by GitHub
parent d76fd103ae
commit 13bdba5d28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 542 additions and 150 deletions

View file

@ -71,6 +71,18 @@ type ListOrSet[T] = list[T] | set[T]
reveal_type(ListOrSet.__type_params__) # revealed: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] 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 ## `TypeAliasType` properties
Two `TypeAliasType`s are distinct and disjoint, even if they refer to the same type 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" # error: [invalid-type-alias-type] "The name of a `typing.TypeAlias` must be a string literal"
IntOrStr = TypeAliasType(get_name(), int | str) 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
```

View file

@ -243,6 +243,7 @@ impl<'db> Completion<'db> {
| Type::KnownInstance(_) | Type::KnownInstance(_)
| Type::AlwaysTruthy | Type::AlwaysTruthy
| Type::AlwaysFalsy => return None, | Type::AlwaysFalsy => return None,
Type::TypeAlias(alias) => imp(db, alias.value_type(db))?,
}) })
} }
imp(db, self.ty) imp(db, self.ty)

View file

@ -605,6 +605,8 @@ pub enum Type<'db> {
TypeIs(TypeIsType<'db>), TypeIs(TypeIsType<'db>),
/// A type that represents an inhabitant of a `TypedDict`. /// A type that represents an inhabitant of a `TypedDict`.
TypedDict(TypedDictType<'db>), TypedDict(TypedDictType<'db>),
/// An aliased type (lazily not-yet-unpacked to its value type).
TypeAlias(TypeAliasType<'db>),
} }
#[salsa::tracked] #[salsa::tracked]
@ -810,6 +812,7 @@ impl<'db> Type<'db> {
// TODO: Materialization of gradual TypedDicts // TODO: Materialization of gradual TypedDicts
*self *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`) // Always normalize single-member enums to their class instance (`Literal[Single.VALUE]` => `Single`)
enum_literal.enum_class_instance(db) enum_literal.enum_class_instance(db)
} }
Type::TypedDict(_) => { Type::TypedDict(_) => {
// TODO: Normalize TypedDicts // TODO: Normalize TypedDicts
self self
} }
Type::TypeAlias(alias) => alias.value_type(db).normalized_impl(db, visitor),
Type::LiteralString Type::LiteralString
| Type::AlwaysFalsy | Type::AlwaysFalsy
| Type::AlwaysTruthy | Type::AlwaysTruthy
@ -1186,7 +1188,8 @@ impl<'db> Type<'db> {
| Type::TypeVar(_) | Type::TypeVar(_)
| Type::BoundSuper(_) | Type::BoundSuper(_)
| Type::TypeIs(_) | 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) enum_literal.enum_class_instance(db).into_callable(db)
} }
Type::TypeAlias(alias) => alias.value_type(db).into_callable(db),
Type::Never Type::Never
| Type::DataclassTransformer(_) | Type::DataclassTransformer(_)
| Type::AlwaysTruthy | 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 { 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 // 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. // 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. // handled above. It's always assignable, though.
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => relation.is_assignability(), (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. // Pretend that instances of `dataclasses.Field` are assignable to their default type.
// This allows field definitions like `name: str = field(default="")` in dataclasses // This allows field definitions like `name: str = field(default="")` in dataclasses
// to pass the assignability check of the inferred type to the declared type. // 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) { match bound_typevar.typevar(db).bound_or_constraints(db) {
None => unreachable!(), None => unreachable!(),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { 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) .typevar(db)
.constraints(db) .constraints(db)
.is_some_and(|constraints| { .is_some_and(|constraints| {
constraints constraints.iter().all(|constraint| {
.iter() self.has_relation_to_impl(db, *constraint, relation, visitor)
.all(|constraint| self.has_relation_to(db, *constraint, relation)) })
}) => }) =>
{ {
true true
@ -1421,12 +1447,12 @@ impl<'db> Type<'db> {
(Type::Union(union), _) => union (Type::Union(union), _) => union
.elements(db) .elements(db)
.iter() .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 (_, Type::Union(union)) => union
.elements(db) .elements(db)
.iter() .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 // 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, // (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 intersection
.positive(db) .positive(db)
.iter() .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 && intersection
.negative(db) .negative(db)
.iter() .iter()
@ -1445,7 +1471,7 @@ impl<'db> Type<'db> {
(Type::Intersection(intersection), _) => intersection (Type::Intersection(intersection), _) => intersection
.positive(db) .positive(db)
.iter() .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 // 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. // 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) self_callable.has_relation_to(db, other_callable, relation)
} }
(_, Type::Callable(_)) => self (_, Type::Callable(_)) => self.into_callable(db).is_some_and(|callable| {
.into_callable(db) callable.has_relation_to_impl(db, target, relation, visitor)
.is_some_and(|callable| callable.has_relation_to(db, target, relation)), }),
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
left.has_relation_to(db, right, relation) left.has_relation_to(db, right, relation)
@ -1541,8 +1567,9 @@ impl<'db> Type<'db> {
| Type::ModuleLiteral(_) | Type::ModuleLiteral(_)
| Type::EnumLiteral(_), | Type::EnumLiteral(_),
_, _,
) => (self.literal_fallback_instance(db)) ) => (self.literal_fallback_instance(db)).is_some_and(|instance| {
.is_some_and(|instance| instance.has_relation_to(db, target, relation)), instance.has_relation_to_impl(db, target, relation, visitor)
}),
// A `FunctionLiteral` type is a single-valued type like the other literals handled above, // 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. // 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: // The same reasoning applies for these special callable types:
(Type::BoundMethod(_), _) => KnownClass::MethodType (Type::BoundMethod(_), _) => KnownClass::MethodType
.to_instance(db) .to_instance(db)
.has_relation_to(db, target, relation), .has_relation_to_impl(db, target, relation, visitor),
(Type::MethodWrapper(_), _) => KnownClass::WrapperDescriptorType (Type::MethodWrapper(_), _) => KnownClass::WrapperDescriptorType
.to_instance(db) .to_instance(db)
.has_relation_to(db, target, relation), .has_relation_to_impl(db, target, relation, visitor),
(Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType (Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType
.to_instance(db) .to_instance(db)
.has_relation_to(db, target, relation), .has_relation_to_impl(db, target, relation, visitor),
(Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => { (Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => {
// TODO: Implement subtyping using an equivalent `Callable` type. // TODO: Implement subtyping using an equivalent `Callable` type.
@ -1568,24 +1595,30 @@ impl<'db> Type<'db> {
// `TypeIs` is invariant. // `TypeIs` is invariant.
(Type::TypeIs(left), Type::TypeIs(right)) => { (Type::TypeIs(left), Type::TypeIs(right)) => {
left.return_type(db) left.return_type(db).has_relation_to_impl(
.has_relation_to(db, right.return_type(db), relation) db,
&& right right.return_type(db),
.return_type(db) relation,
.has_relation_to(db, left.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`. // `TypeIs[T]` is a subtype of `bool`.
(Type::TypeIs(_), _) => KnownClass::Bool (Type::TypeIs(_), _) => KnownClass::Bool
.to_instance(db) .to_instance(db)
.has_relation_to(db, target, relation), .has_relation_to_impl(db, target, relation, visitor),
// Function-like callables are subtypes of `FunctionType` // Function-like callables are subtypes of `FunctionType`
(Type::Callable(callable), _) (Type::Callable(callable), _)
if callable.is_function_like(db) if callable.is_function_like(db)
&& KnownClass::FunctionType && KnownClass::FunctionType
.to_instance(db) .to_instance(db)
.has_relation_to(db, target, relation) => .has_relation_to_impl(db, target, relation, visitor) =>
{ {
true true
} }
@ -1595,7 +1628,7 @@ impl<'db> Type<'db> {
(Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target), (Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target),
(Type::BoundSuper(_), _) => KnownClass::Super (Type::BoundSuper(_), _) => KnownClass::Super
.to_instance(db) .to_instance(db)
.has_relation_to(db, target, relation), .has_relation_to_impl(db, target, relation, visitor),
// `Literal[<class 'C'>]` is a subtype of `type[B]` if `C` is a subclass of `B`, // `Literal[<class 'C'>]` 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`. // since `type[B]` describes all possible runtime subclasses of the class object `B`.
@ -1603,20 +1636,30 @@ impl<'db> Type<'db> {
.subclass_of() .subclass_of()
.into_class() .into_class()
.map(|subclass_of_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()), .unwrap_or(relation.is_assignability()),
(Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
.subclass_of() .subclass_of()
.into_class() .into_class()
.map(|subclass_of_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()), .unwrap_or(relation.is_assignability()),
// This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`? // 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)) => { (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`. // `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`. // is an instance of its metaclass `abc.ABCMeta`.
(Type::ClassLiteral(class), _) => class (Type::ClassLiteral(class), _) => class
.metaclass_instance_type(db) .metaclass_instance_type(db)
.has_relation_to(db, target, relation), .has_relation_to_impl(db, target, relation, visitor),
(Type::GenericAlias(alias), _) => ClassType::from(alias) (Type::GenericAlias(alias), _) => ClassType::from(alias)
.metaclass_instance_type(db) .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[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() => { (Type::SubclassOf(subclass_of_ty), other) if subclass_of_ty.is_dynamic() => {
KnownClass::Type KnownClass::Type
.to_instance(db) .to_instance(db)
.has_relation_to(db, other, relation) .has_relation_to_impl(db, other, relation, visitor)
|| (relation.is_assignability() || (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]` // Any `type[...]` type is assignable to `type[Any]`
(other, Type::SubclassOf(subclass_of_ty)) (other, Type::SubclassOf(subclass_of_ty))
if subclass_of_ty.is_dynamic() && relation.is_assignability() => 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 // `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses
@ -1657,31 +1705,36 @@ impl<'db> Type<'db> {
.into_class() .into_class()
.map(|class| class.metaclass_instance_type(db)) .map(|class| class.metaclass_instance_type(db))
.unwrap_or_else(|| KnownClass::Type.to_instance(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)`, // 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 // 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. // (the symbol `typing.Type`), and that symbol is known to be an instance of `typing._SpecialForm` at runtime.
(Type::SpecialForm(left), right) => left (Type::SpecialForm(left), right) => left
.instance_fallback(db) .instance_fallback(db)
.has_relation_to(db, right, relation), .has_relation_to_impl(db, right, relation, visitor),
(Type::KnownInstance(left), right) => left (Type::KnownInstance(left), right) => left
.instance_fallback(db) .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`, // `bool` is a subtype of `int`, because `bool` subclasses `int`,
// which means that all instances of `bool` are also instances of `int` // which means that all instances of `bool` are also instances of `int`
(Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => { (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 (Type::PropertyInstance(_), _) => KnownClass::Property
.to_instance(db) .to_instance(db)
.has_relation_to(db, target, relation), .has_relation_to_impl(db, target, relation, visitor),
(_, Type::PropertyInstance(_)) => { (_, Type::PropertyInstance(_)) => self.has_relation_to_impl(
self.has_relation_to(db, KnownClass::Property.to_instance(db), relation) db,
} KnownClass::Property.to_instance(db),
relation,
visitor,
),
// Other than the special cases enumerated above, `Instance` types and typevars are // Other than the special cases enumerated above, `Instance` types and typevars are
// never subtypes of any other variants // 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 /// [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 { 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 { if self == other {
return true; 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)) => { (Type::NominalInstance(first), Type::NominalInstance(second)) => {
first.is_equivalent_to(db, second) first.is_equivalent_to(db, second)
} }
@ -1801,6 +1877,20 @@ impl<'db> Type<'db> {
false 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 // 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 // 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 // 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::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) any_protocol_members_absent_or_disjoint(db, protocol, special_form.instance_fallback(db), visitor)
} }),
(Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance)) (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) 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 // The absence of a protocol member on one of these types guarantees
// that the type will be disjoint from the protocol, // that the type will be disjoint from the protocol,
@ -2006,27 +2097,27 @@ impl<'db> Type<'db> {
| Type::GenericAlias(..) | Type::GenericAlias(..)
| Type::IntLiteral(..) | Type::IntLiteral(..)
| Type::EnumLiteral(..)), | 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 -- // This is the same as the branch above --
// once guard patterns are stabilised, it could be unified with that branch // once guard patterns are stabilised, it could be unified with that branch
// (<https://github.com/rust-lang/rust/issues/129967>) // (<https://github.com/rust-lang/rust/issues/129967>)
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) | (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) any_protocol_members_absent_or_disjoint(db, protocol, nominal, visitor)
} }),
(Type::ProtocolInstance(protocol), other) (Type::ProtocolInstance(protocol), other)
| (other, Type::ProtocolInstance(protocol)) => { | (other, Type::ProtocolInstance(protocol)) => visitor.visit((self, other), || {
protocol.interface(db).members(db).any(|member| { protocol.interface(db).members(db).any(|member| {
matches!( matches!(
other.member(db, member.name()).place, other.member(db, member.name()).place,
Place::Type(attribute_type, _) if member.has_disjoint_type_from(db, attribute_type, visitor) Place::Type(attribute_type, _) if member.has_disjoint_type_from(db, attribute_type, visitor)
) )
}) })
} }),
(Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b)) (Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b))
| (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => {
@ -2349,6 +2440,7 @@ impl<'db> Type<'db> {
Type::AlwaysTruthy | Type::AlwaysFalsy => false, Type::AlwaysTruthy | Type::AlwaysFalsy => false,
Type::TypeIs(type_is) => type_is.is_bound(db), Type::TypeIs(type_is) => type_is.is_bound(db),
Type::TypedDict(_) => false, 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::TypeIs(type_is) => type_is.is_bound(db),
Type::TypeAlias(alias) => alias.value_type(db).is_single_valued(db),
Type::Dynamic(_) Type::Dynamic(_)
| Type::Never | Type::Never
| Type::Union(..) | 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::FunctionLiteral(_)
| Type::Callable(_) | Type::Callable(_)
| Type::BoundMethod(_) | Type::BoundMethod(_)
@ -2726,6 +2824,8 @@ impl<'db> Type<'db> {
} }
Type::TypedDict(_) => Place::Unbound.into(), 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, policy,
), ),
Type::TypeAlias(alias) => alias
.value_type(db)
.member_lookup_with_policy(db, name, policy),
Type::NominalInstance(..) Type::NominalInstance(..)
| Type::ProtocolInstance(..) | Type::ProtocolInstance(..)
| Type::BooleanLiteral(..) | Type::BooleanLiteral(..)
@ -3580,6 +3684,9 @@ impl<'db> Type<'db> {
Type::BooleanLiteral(bool) => Truthiness::from(*bool), Type::BooleanLiteral(bool) => Truthiness::from(*bool),
Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()), Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()),
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.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) Ok(truthiness)
@ -4537,6 +4644,8 @@ impl<'db> Type<'db> {
known_instance.instance_fallback(db).bindings(db) known_instance.instance_fallback(db).bindings(db)
} }
Type::TypeAlias(alias) => alias.value_type(db).bindings(db),
Type::PropertyInstance(_) Type::PropertyInstance(_)
| Type::AlwaysFalsy | Type::AlwaysFalsy
| Type::AlwaysTruthy | Type::AlwaysTruthy
@ -5172,6 +5281,7 @@ impl<'db> Type<'db> {
bound_typevar.binding_context(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::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance")),
Type::BooleanLiteral(_) Type::BooleanLiteral(_)
| Type::BytesLiteral(_) | Type::BytesLiteral(_)
@ -5275,7 +5385,7 @@ impl<'db> Type<'db> {
}), }),
Type::KnownInstance(known_instance) => match known_instance { 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) => { KnownInstanceType::TypeVar(typevar) => {
let module = parsed_module(db, scope_id.file(db)).load(db); let module = parsed_module(db, scope_id.file(db)).load(db);
let index = semantic_index(db, scope_id.file(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::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::BoundSuper(_) => KnownClass::Super.to_class_literal(db),
Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), Type::ProtocolInstance(protocol) => protocol.to_meta_type(db),
Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class), 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, self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, '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> { ) -> Type<'db> {
match self { match self {
Type::TypeVar(bound_typevar) => match type_mapping { Type::TypeVar(bound_typevar) => match type_mapping {
@ -5650,10 +5776,10 @@ impl<'db> Type<'db> {
)), )),
Type::NominalInstance(instance) => 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) => {
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)) => { Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
@ -5685,15 +5811,15 @@ impl<'db> Type<'db> {
} }
Type::GenericAlias(generic) => { 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) => {
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( 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) => { Type::PropertyInstance(property) => {
@ -5701,23 +5827,27 @@ impl<'db> Type<'db> {
} }
Type::Union(union) => union.map(db, |element| { 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) => { Type::Intersection(intersection) => {
let mut builder = IntersectionBuilder::new(db); let mut builder = IntersectionBuilder::new(db);
for positive in intersection.positive(db) { for positive in intersection.positive(db) {
builder = 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) { for negative in intersection.negative(db) {
builder = 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() builder.build()
} }
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping)), 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::ModuleLiteral(_)
| Type::IntLiteral(_) | Type::IntLiteral(_)
| Type::BooleanLiteral(_) | Type::BooleanLiteral(_)
@ -5842,6 +5972,12 @@ impl<'db> Type<'db> {
.find_legacy_typevars(db, binding_context, typevars); .find_legacy_typevars(db, binding_context, typevars);
} }
Type::TypeAlias(alias) => {
alias
.value_type(db)
.find_legacy_typevars(db, binding_context, typevars);
}
Type::Dynamic(_) Type::Dynamic(_)
| Type::Never | Type::Never
| Type::AlwaysTruthy | Type::AlwaysTruthy
@ -5954,6 +6090,8 @@ impl<'db> Type<'db> {
SubclassOfInner::Dynamic(_) => None, SubclassOfInner::Dynamic(_) => None,
}, },
Self::TypeAlias(alias) => alias.value_type(db).definition(db),
Self::StringLiteral(_) Self::StringLiteral(_)
| Self::BooleanLiteral(_) | Self::BooleanLiteral(_)
| Self::LiteralString | Self::LiteralString
@ -8412,7 +8550,7 @@ impl<'db> PEP695TypeAliasType<'db> {
semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) 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> { pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> {
let scope = self.rhs_scope(db); let scope = self.rhs_scope(db);
let module = parsed_module(db, scope.file(db)).load(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<Type<'db>> {
salsa::CycleRecoveryAction::Iterate
}
fn value_type_cycle_initial<'db>(_db: &'db dyn Db, _self: PEP695TypeAliasType<'db>) -> Type<'db> {
Type::Never
}
/// # Ordering /// # Ordering
/// Ordering is based on the type alias's salsa-assigned id and not on its values. /// 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. /// 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, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update, get_size2::GetSize,
)] )]
pub enum TypeAliasType<'db> { pub enum TypeAliasType<'db> {
/// A type alias defined using the PEP 695 `type` statement.
PEP695(PEP695TypeAliasType<'db>), PEP695(PEP695TypeAliasType<'db>),
/// A type alias defined by manually instantiating the PEP 695 `types.TypeAliasType`.
Bare(BareTypeAliasType<'db>), Bare(BareTypeAliasType<'db>),
} }
@ -8547,7 +8700,7 @@ impl get_size2::GetSize for UnionType<'_> {}
impl<'db> UnionType<'db> { impl<'db> UnionType<'db> {
/// Create a union from a list of elements /// Create a union from a list of elements
/// (which may be eagerly simplified into a different variant of [`Type`] altogether). /// (which may be eagerly simplified into a different variant of [`Type`] altogether).
pub fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db> pub(crate) fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
where where
I: IntoIterator<Item = T>, I: IntoIterator<Item = T>,
T: Into<Type<'db>>, T: Into<Type<'db>>,
@ -9037,13 +9190,16 @@ impl<'db> TypedDictType<'db> {
class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict) class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict)
} }
pub(crate) fn apply_type_mapping<'a>( pub(crate) fn apply_type_mapping_impl<'a>(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>, type_mapping: &TypeMapping<'a, 'db>,
visitor: &TypeTransformer<'db>,
) -> Self { ) -> Self {
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),
} }
} }
} }

View file

@ -246,6 +246,7 @@ impl<'db> UnionBuilder<'db> {
} }
// Adding `Never` to a union is a no-op. // Adding `Never` to a union is a no-op.
Type::Never => {} 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 // 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 // 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` // 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 { pub(crate) fn add_positive(mut self, ty: Type<'db>) -> Self {
match ty { match ty {
Type::TypeAlias(alias) => {
let value_type = alias.value_type(self.db);
self.add_positive(value_type)
}
Type::Union(union) => { Type::Union(union) => {
// Distribute ourself over this union: for each union element, clone ourself and // 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 // 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. // See comments above in `add_positive`; this is just the negated version.
match ty { match ty {
Type::TypeAlias(alias) => {
let value_type = alias.value_type(self.db);
self.add_negative(value_type)
}
Type::Union(union) => { Type::Union(union) => {
for elem in union.elements(self.db) { for elem in union.elements(self.db) {
self = self.add_negative(*elem); self = self.add_negative(*elem);

View file

@ -43,7 +43,7 @@ use crate::{
}, },
types::{ types::{
CallArguments, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType, CallArguments, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType,
definition_expression_type, cyclic::PairVisitor, definition_expression_type,
}, },
}; };
use indexmap::IndexSet; use indexmap::IndexSet;
@ -251,15 +251,17 @@ impl<'db> GenericAlias<'db> {
self.origin(db).definition(db) self.origin(db).definition(db)
} }
pub(super) fn apply_type_mapping<'a>( pub(super) fn apply_type_mapping_impl<'a>(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>, type_mapping: &TypeMapping<'a, 'db>,
visitor: &TypeTransformer<'db>,
) -> Self { ) -> Self {
Self::new( Self::new(
db, db,
self.origin(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) self.is_known(db, KnownClass::Object)
} }
pub(super) fn apply_type_mapping<'a>( pub(super) fn apply_type_mapping_impl<'a>(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>, type_mapping: &TypeMapping<'a, 'db>,
visitor: &TypeTransformer<'db>,
) -> Self { ) -> Self {
match self { match self {
Self::NonGeneric(_) => 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. /// 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 { 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, self,
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
relation: TypeRelation, relation: TypeRelation,
visitor: &PairVisitor<'db>,
) -> bool { ) -> bool {
self.iter_mro(db).any(|base| { self.iter_mro(db).any(|base| {
match base { match base {
@ -479,10 +485,11 @@ impl<'db> ClassType<'db> {
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other, (ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
(ClassType::Generic(base), ClassType::Generic(other)) => { (ClassType::Generic(base), ClassType::Generic(other)) => {
base.origin(db) == other.origin(db) base.origin(db) == other.origin(db)
&& base.specialization(db).has_relation_to( && base.specialization(db).has_relation_to_impl(
db, db,
other.specialization(db), other.specialization(db),
relation, relation,
visitor,
) )
} }
(ClassType::Generic(_), ClassType::NonGeneric(_)) (ClassType::Generic(_), ClassType::NonGeneric(_))

View file

@ -135,6 +135,8 @@ impl<'db> ClassBase<'db> {
// in which case we want to treat `Never` in a forgiving way and silence diagnostics // in which case we want to treat `Never` in a forgiving way and silence diagnostics
Type::Never => Some(ClassBase::unknown()), Type::Never => Some(ClassBase::unknown()),
Type::TypeAlias(alias) => Self::try_from_type(db, alias.value_type(db)),
Type::PropertyInstance(_) Type::PropertyInstance(_)
| Type::BooleanLiteral(_) | Type::BooleanLiteral(_)
| Type::FunctionLiteral(_) | 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 { 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, Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => self,
} }
} }
@ -260,7 +269,11 @@ impl<'db> ClassBase<'db> {
specialization: Option<Specialization<'db>>, specialization: Option<Specialization<'db>>,
) -> Self { ) -> Self {
if let Some(specialization) = specialization { 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 { } else {
self self
} }

View file

@ -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 rustc_hash::FxHashMap;
use crate::FxIndexSet; use crate::FxIndexSet;
@ -22,17 +42,17 @@ pub(crate) type PairVisitor<'db> = CycleDetector<(Type<'db>, Type<'db>), bool>;
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct CycleDetector<T, R> { pub(crate) struct CycleDetector<T, R> {
/// If the type we're visiting is present in `seen`, /// If the type we're visiting is present in `seen`, it indicates that we've hit a cycle (due
/// it indicates that we've hit a cycle (due to a recursive type); /// to a recursive type); we need to immediately short circuit the whole operation and return
/// we need to immediately short circuit the whole operation and return the fallback value. /// the fallback value. That's why we pop items off the end of `seen` after we've visited them.
/// That's why we pop items off the end of `seen` after we've visited them.
seen: RefCell<FxIndexSet<T>>, seen: RefCell<FxIndexSet<T>>,
/// Unlike `seen`, this field is a pure performance optimisation (and an essential one). /// Unlike `seen`, this field is a pure performance optimisation (and an essential one). If the
/// If the type we're trying to normalize is present in `cache`, it doesn't necessarily mean we've hit a cycle: /// type we're trying to normalize is present in `cache`, it doesn't necessarily mean we've hit
/// it just means that we've already visited this inner type as part of a bigger call chain we're currently in. /// a cycle: it just means that we've already visited this inner type as part of a bigger call
/// Since this cache is just a performance optimisation, it doesn't make sense to pop items off the end of the /// chain we're currently in. Since this cache is just a performance optimisation, it doesn't
/// cache after they've been visited (it would sort-of defeat the point of a cache if we did!) /// 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<FxHashMap<T, R>>, cache: RefCell<FxHashMap<T, R>>,
fallback: R, fallback: R,
@ -48,8 +68,8 @@ impl<T: Hash + Eq + Copy, R: Copy> CycleDetector<T, R> {
} }
pub(crate) fn visit(&self, item: T, func: impl FnOnce() -> R) -> R { pub(crate) fn visit(&self, item: T, func: impl FnOnce() -> R) -> R {
if let Some(ty) = self.cache.borrow().get(&item) { if let Some(val) = self.cache.borrow().get(&item) {
return *ty; return *val;
} }
// We hit a cycle // We hit a cycle

View file

@ -243,6 +243,7 @@ impl Display for DisplayRepresentation<'_> {
f.write_str("]") f.write_str("]")
} }
Type::TypedDict(typed_dict) => f.write_str(typed_dict.defining_class.name(self.db)), Type::TypedDict(typed_dict) => f.write_str(typed_dict.defining_class.name(self.db)),
Type::TypeAlias(alias) => f.write_str(alias.name(self.db)),
} }
} }
} }

View file

@ -1000,6 +1000,8 @@ fn is_instance_truthiness<'db>(
Type::ClassLiteral(..) => always_true_if(is_instance(&KnownClass::Type.to_instance(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::BoundMethod(..)
| Type::MethodWrapper(..) | Type::MethodWrapper(..)
| Type::WrapperDescriptor(..) | Type::WrapperDescriptor(..)

View file

@ -16,7 +16,7 @@ use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
use crate::types::{ use crate::types::{
BoundTypeVarInstance, KnownClass, KnownInstanceType, Type, TypeMapping, TypeRelation, BoundTypeVarInstance, KnownClass, KnownInstanceType, Type, TypeMapping, TypeRelation,
TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType, TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType,
binding_type, declaration_type, binding_type, cyclic::PairVisitor, declaration_type,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
@ -304,9 +304,9 @@ impl<'db> GenericContext<'db> {
pub(crate) fn specialize_tuple( pub(crate) fn specialize_tuple(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
element_type: Type<'db>,
tuple: TupleType<'db>, tuple: TupleType<'db>,
) -> Specialization<'db> { ) -> Specialization<'db> {
let element_type = tuple.tuple(db).homogeneous_element_type(db);
Specialization::new(db, self, Box::from([element_type]), Some(tuple)) Specialization::new(db, self, Box::from([element_type]), Some(tuple))
} }
@ -463,15 +463,24 @@ impl<'db> Specialization<'db> {
self, self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, '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 { ) -> Self {
let types: Box<[_]> = self let types: Box<[_]> = self
.types(db) .types(db)
.iter() .iter()
.map(|ty| ty.apply_type_mapping(db, type_mapping)) .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor))
.collect(); .collect();
let tuple_inner = self let tuple_inner = self
.tuple_inner(db) .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) 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) Specialization::new(db, self.generic_context(db), types, tuple_inner)
} }
pub(crate) fn has_relation_to( pub(crate) fn has_relation_to_impl(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
relation: TypeRelation, relation: TypeRelation,
visitor: &PairVisitor<'db>,
) -> bool { ) -> bool {
let generic_context = self.generic_context(db); let generic_context = self.generic_context(db);
if generic_context != other.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)) 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()) 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) && 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 => { 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, TypeVarVariance::Bivariant => true,
}; };

View file

@ -132,6 +132,8 @@ impl<'db> AllMembers<'db> {
Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy | Type::AlwaysFalsy => {} Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy | Type::AlwaysFalsy => {}
Type::TypeAlias(alias) => self.extend_with_type(db, alias.value_type(db)),
Type::IntLiteral(_) Type::IntLiteral(_)
| Type::BooleanLiteral(_) | Type::BooleanLiteral(_)
| Type::StringLiteral(_) | Type::StringLiteral(_)

View file

@ -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 // Super instances do not allow attribute assignment
Type::NominalInstance(instance) Type::NominalInstance(instance)
if instance.class(db).is_known(db, KnownClass::Super) => 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); 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) { match (op, operand_type) {
(_, Type::Dynamic(_)) => operand_type, (_, Type::Dynamic(_)) => operand_type,
(_, Type::Never) => Type::Never, (_, 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::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value),
(ast::UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value), (ast::UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value),
(ast::UnaryOp::Invert, 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, // 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). // the result would then become Any or Unknown, respectively).
(any @ Type::Dynamic(DynamicType::Any), _, _) (any @ Type::Dynamic(DynamicType::Any), _, _)

View file

@ -260,20 +260,21 @@ impl<'db> NominalInstanceType<'db> {
} }
} }
pub(super) fn has_relation_to( pub(super) fn has_relation_to_impl(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
relation: TypeRelation, relation: TypeRelation,
visitor: &PairVisitor<'db>,
) -> bool { ) -> bool {
match (self.0, other.0) { match (self.0, other.0) {
( (
NominalInstanceInner::ExactTuple(tuple1), NominalInstanceInner::ExactTuple(tuple1),
NominalInstanceInner::ExactTuple(tuple2), NominalInstanceInner::ExactTuple(tuple2),
) => tuple1.has_relation_to(db, tuple2, relation), ) => tuple1.has_relation_to_impl(db, tuple2, relation, visitor),
_ => self _ => self
.class(db) .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)) SubclassOfType::from(db, self.class(db))
} }
pub(super) fn apply_type_mapping<'a>( pub(super) fn apply_type_mapping_impl<'a>(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>, type_mapping: &TypeMapping<'a, 'db>,
visitor: &TypeTransformer<'db>,
) -> Type<'db> { ) -> Type<'db> {
match self.0 { match self.0 {
NominalInstanceInner::ExactTuple(tuple) => { 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) => { 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, self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>, type_mapping: &TypeMapping<'a, 'db>,
visitor: &TypeTransformer<'db>,
) -> Self { ) -> Self {
match self.inner { match self.inner {
Protocol::FromClass(class) => { 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) => { 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)) Self(self.0.materialize(db, variance))
} }
pub(super) fn apply_type_mapping<'a>( pub(super) fn apply_type_mapping_impl<'a>(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>, type_mapping: &TypeMapping<'a, 'db>,
_visitor: &TypeTransformer<'db>,
) -> Self { ) -> Self {
Self(self.0.specialized_and_normalized(db, type_mapping)) Self(self.0.specialized_and_normalized(db, type_mapping))
} }

View file

@ -182,6 +182,7 @@ impl ClassInfoConstraintFunction {
}; };
match classinfo { match classinfo {
Type::TypeAlias(alias) => self.generate_constraint(db, alias.value_type(db)),
Type::ClassLiteral(class_literal) => { Type::ClassLiteral(class_literal) => {
// At runtime (on Python 3.11+), this will return `True` for classes that actually // 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? // do inherit `typing.Any` and `False` otherwise. We could accurately model that?

View file

@ -437,9 +437,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
match &self.kind { match &self.kind {
// TODO: implement disjointness for property/method members as well as attribute members // TODO: implement disjointness for property/method members as well as attribute members
ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => false, ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => false,
ProtocolMemberKind::Other(ty) => visitor.visit((*ty, other), || { ProtocolMemberKind::Other(ty) => ty.is_disjoint_from_impl(db, other, visitor),
ty.is_disjoint_from_impl(db, other, visitor)
}),
} }
} }

View file

@ -4,7 +4,7 @@ use crate::place::PlaceAndQualifiers;
use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::Definition;
use crate::types::{ use crate::types::{
BindingContext, BoundTypeVarInstance, ClassType, DynamicType, KnownClass, MemberLookupPolicy, BindingContext, BoundTypeVarInstance, ClassType, DynamicType, KnownClass, MemberLookupPolicy,
Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, cyclic::PairVisitor,
}; };
use crate::{Db, FxOrderSet}; 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, self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>, type_mapping: &TypeMapping<'a, 'db>,
visitor: &TypeTransformer<'db>,
) -> Self { ) -> Self {
match self.subclass_of { match self.subclass_of {
SubclassOfInner::Class(class) => Self { 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, SubclassOfInner::Dynamic(_) => self,
} }
@ -149,11 +154,12 @@ impl<'db> SubclassOfType<'db> {
} }
/// Return `true` if `self` has a certain relation to `other`. /// Return `true` if `self` has a certain relation to `other`.
pub(crate) fn has_relation_to( pub(crate) fn has_relation_to_impl(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
other: SubclassOfType<'db>, other: SubclassOfType<'db>,
relation: TypeRelation, relation: TypeRelation,
visitor: &PairVisitor<'db>,
) -> bool { ) -> bool {
match (self.subclass_of, other.subclass_of) { match (self.subclass_of, other.subclass_of) {
(SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => { (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => {
@ -168,7 +174,7 @@ impl<'db> SubclassOfType<'db> {
// and `type[int]` describes all possible runtime subclasses of the class `int`. // 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`. // 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)) => { (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)
} }
} }
} }

View file

@ -223,7 +223,7 @@ impl<'db> TupleType<'db> {
// N.B. If this method is not Salsa-tracked, we take 10 minutes to check // 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 // `static-frame` as part of a mypy_primer run! This is because it's called
// from `NominalInstanceType::class()`, which is a very hot method. // 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> { pub(crate) fn to_class_type(self, db: &'db dyn Db) -> ClassType<'db> {
let tuple_class = KnownClass::Tuple let tuple_class = KnownClass::Tuple
.try_to_class_literal(db) .try_to_class_literal(db)
@ -231,7 +231,8 @@ impl<'db> TupleType<'db> {
tuple_class.apply_specialization(db, |generic_context| { tuple_class.apply_specialization(db, |generic_context| {
if generic_context.variables(db).len() == 1 { 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 { } else {
generic_context.default_specialization(db, Some(KnownClass::Tuple)) 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)) 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, self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>, type_mapping: &TypeMapping<'a, 'db>,
visitor: &TypeTransformer<'db>,
) -> Option<Self> { ) -> Option<Self> {
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( pub(crate) fn find_legacy_typevars(
@ -272,14 +278,15 @@ impl<'db> TupleType<'db> {
.find_legacy_typevars(db, binding_context, typevars); .find_legacy_typevars(db, binding_context, typevars);
} }
pub(crate) fn has_relation_to( pub(crate) fn has_relation_to_impl(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
relation: TypeRelation, relation: TypeRelation,
visitor: &PairVisitor<'db>,
) -> bool { ) -> bool {
self.tuple(db) 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 { 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<ClassType<'db>> {
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. /// 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 /// 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<Type<'db>> {
Self::from_elements(self.0.iter().map(|ty| ty.materialize(db, variance))) 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::from_elements(
self.0 self.0
.iter() .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<Type<'db>> {
} }
} }
fn has_relation_to( fn has_relation_to_impl(
&self, &self,
db: &'db dyn Db, db: &'db dyn Db,
other: &Tuple<Type<'db>>, other: &Tuple<Type<'db>>,
relation: TypeRelation, relation: TypeRelation,
visitor: &PairVisitor<'db>,
) -> bool { ) -> bool {
match other { match other {
Tuple::Fixed(other) => { Tuple::Fixed(other) => {
self.0.len() == other.0.len() self.0.len() == other.0.len()
&& (self.0.iter()) && (self.0.iter()).zip(&other.0).all(|(self_ty, other_ty)| {
.zip(&other.0) self_ty.has_relation_to_impl(db, *other_ty, relation, visitor)
.all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation)) })
} }
Tuple::Variable(other) => { Tuple::Variable(other) => {
@ -420,7 +456,7 @@ impl<'db> FixedLengthTuple<Type<'db>> {
let Some(self_ty) = self_iter.next() else { let Some(self_ty) = self_iter.next() else {
return false; 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; return false;
} }
} }
@ -428,14 +464,16 @@ impl<'db> FixedLengthTuple<Type<'db>> {
let Some(self_ty) = self_iter.next_back() else { let Some(self_ty) = self_iter.next_back() else {
return false; 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; return false;
} }
} }
// In addition, any remaining elements in this tuple must satisfy the // In addition, any remaining elements in this tuple must satisfy the
// variable-length portion of the other tuple. // 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<Type<'db>> {
) )
} }
fn apply_type_mapping<'a>( fn apply_type_mapping_impl<'a>(
&self, &self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>, type_mapping: &TypeMapping<'a, 'db>,
visitor: &TypeTransformer<'db>,
) -> TupleSpec<'db> { ) -> TupleSpec<'db> {
Self::mixed( Self::mixed(
self.prefix self.prefix
.iter() .iter()
.map(|ty| ty.apply_type_mapping(db, type_mapping)), .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)),
self.variable.apply_type_mapping(db, type_mapping), self.variable
.apply_type_mapping_impl(db, type_mapping, visitor),
self.suffix self.suffix
.iter() .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<Type<'db>> {
} }
} }
fn has_relation_to( fn has_relation_to_impl(
&self, &self,
db: &'db dyn Db, db: &'db dyn Db,
other: &Tuple<Type<'db>>, other: &Tuple<Type<'db>>,
relation: TypeRelation, relation: TypeRelation,
visitor: &PairVisitor<'db>,
) -> bool { ) -> bool {
match other { match other {
Tuple::Fixed(other) => { Tuple::Fixed(other) => {
@ -732,7 +773,7 @@ impl<'db> VariableLengthTuple<Type<'db>> {
let Some(other_ty) = other_iter.next() else { let Some(other_ty) = other_iter.next() else {
return false; 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; return false;
} }
} }
@ -741,7 +782,7 @@ impl<'db> VariableLengthTuple<Type<'db>> {
let Some(other_ty) = other_iter.next_back() else { let Some(other_ty) = other_iter.next_back() else {
return false; 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; return false;
} }
} }
@ -771,10 +812,10 @@ impl<'db> VariableLengthTuple<Type<'db>> {
) )
.all(|pair| match pair { .all(|pair| match pair {
EitherOrBoth::Both(self_ty, other_ty) => { 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) => { 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(_) => { EitherOrBoth::Right(_) => {
// The rhs has a required element that the lhs is not guaranteed to // The rhs has a required element that the lhs is not guaranteed to
@ -796,10 +837,10 @@ impl<'db> VariableLengthTuple<Type<'db>> {
.zip_longest(other_suffix.iter().rev()) .zip_longest(other_suffix.iter().rev())
.all(|pair| match pair { .all(|pair| match pair {
EitherOrBoth::Both(self_ty, other_ty) => { 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) => { 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(_) => { EitherOrBoth::Right(_) => {
// The rhs has a required element that the lhs is not guaranteed to // The rhs has a required element that the lhs is not guaranteed to
@ -812,7 +853,8 @@ impl<'db> VariableLengthTuple<Type<'db>> {
} }
// And lastly, the variable-length portions must satisfy the relation. // 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<Type<'db>> {
} }
} }
pub(crate) fn apply_type_mapping<'a>( pub(crate) fn apply_type_mapping_impl<'a>(
&self, &self,
db: &'db dyn Db, db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>, type_mapping: &TypeMapping<'a, 'db>,
visitor: &TypeTransformer<'db>,
) -> Self { ) -> Self {
match self { match self {
Tuple::Fixed(tuple) => Tuple::Fixed(tuple.apply_type_mapping(db, type_mapping)), Tuple::Fixed(tuple) => {
Tuple::Variable(tuple) => tuple.apply_type_mapping(db, type_mapping), 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<Type<'db>> {
} }
} }
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 { match self {
Tuple::Fixed(self_tuple) => self_tuple.has_relation_to(db, other, relation), Tuple::Fixed(self_tuple) => {
Tuple::Variable(self_tuple) => self_tuple.has_relation_to(db, other, relation), self_tuple.has_relation_to_impl(db, other, relation, visitor)
}
Tuple::Variable(self_tuple) => {
self_tuple.has_relation_to_impl(db, other, relation, visitor)
}
} }
} }

View file

@ -204,6 +204,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::Dynamic(_), _) => Ordering::Less, (Type::Dynamic(_), _) => Ordering::Less,
(_, Type::Dynamic(_)) => Ordering::Greater, (_, 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(_)) => { (Type::Union(_), _) | (_, Type::Union(_)) => {
unreachable!("our type representation does not permit nested unions"); unreachable!("our type representation does not permit nested unions");
} }

View file

@ -117,6 +117,7 @@ enum NonAtomicType<'db> {
TypeVar(BoundTypeVarInstance<'db>), TypeVar(BoundTypeVarInstance<'db>),
ProtocolInstance(ProtocolInstanceType<'db>), ProtocolInstance(ProtocolInstanceType<'db>),
TypedDict(TypedDictType<'db>), TypedDict(TypedDictType<'db>),
TypeAlias(TypeAliasType<'db>),
} }
enum TypeKind<'db> { enum TypeKind<'db> {
@ -183,6 +184,7 @@ impl<'db> From<Type<'db>> for TypeKind<'db> {
Type::TypedDict(typed_dict) => { Type::TypedDict(typed_dict) => {
TypeKind::NonAtomic(NonAtomicType::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); visitor.visit_protocol_instance_type(db, protocol);
} }
NonAtomicType::TypedDict(typed_dict) => visitor.visit_typed_dict_type(db, typed_dict), NonAtomicType::TypedDict(typed_dict) => visitor.visit_typed_dict_type(db, typed_dict),
NonAtomicType::TypeAlias(alias) => {
visitor.visit_type_alias_type(db, alias);
}
} }
} }