[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, ...]
```
## 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
```

View file

@ -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)

View file

@ -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[<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`.
@ -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
// (<https://github.com/rust-lang/rust/issues/129967>)
(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<Type<'db>> {
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<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
I: IntoIterator<Item = T>,
T: Into<Type<'db>>,
@ -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),
}
}
}

View file

@ -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);

View file

@ -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(_))

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
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<Specialization<'db>>,
) -> 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
}

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 crate::FxIndexSet;
@ -22,17 +42,17 @@ pub(crate) type PairVisitor<'db> = CycleDetector<(Type<'db>, Type<'db>), bool>;
#[derive(Debug)]
pub(crate) struct CycleDetector<T, R> {
/// 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<FxIndexSet<T>>,
/// 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<FxHashMap<T, 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 {
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

View file

@ -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)),
}
}
}

View file

@ -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(..)

View file

@ -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,
};

View file

@ -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(_)

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
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), _, _)

View file

@ -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))
}

View file

@ -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?

View file

@ -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),
}
}

View file

@ -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)
}
}
}

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
// `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<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(
@ -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<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.
///
/// 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)))
}
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<Type<'db>> {
}
}
fn has_relation_to(
fn has_relation_to_impl(
&self,
db: &'db dyn Db,
other: &Tuple<Type<'db>>,
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<Type<'db>> {
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<Type<'db>> {
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<Type<'db>> {
)
}
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<Type<'db>> {
}
}
fn has_relation_to(
fn has_relation_to_impl(
&self,
db: &'db dyn Db,
other: &Tuple<Type<'db>>,
relation: TypeRelation,
visitor: &PairVisitor<'db>,
) -> bool {
match other {
Tuple::Fixed(other) => {
@ -732,7 +773,7 @@ impl<'db> VariableLengthTuple<Type<'db>> {
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<Type<'db>> {
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<Type<'db>> {
)
.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<Type<'db>> {
.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<Type<'db>> {
}
// 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,
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<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 {
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)
}
}
}

View file

@ -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");
}

View file

@ -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<Type<'db>> 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);
}
}
}