[ty] Support recursive assignment-based type aliases

This commit is contained in:
Charlie Marsh
2026-05-03 22:43:40 -07:00
parent c609bd8a26
commit cbd31b3f4e
21 changed files with 975 additions and 100 deletions
@@ -301,7 +301,7 @@ python-version = "3.12"
```py
from __future__ import annotations
type RecursiveNewReturn = RecursiveNew | RecursiveNewReturn
type RecursiveNewReturn = RecursiveNew | RecursiveNewReturn # error: [cyclic-type-alias-definition]
class RecursiveNew:
def __new__(cls, value: int) -> RecursiveNewReturn:
@@ -323,7 +323,7 @@ python-version = "3.12"
```py
from __future__ import annotations
type RecursiveMetaCallReturn = RecursiveMetaCall | RecursiveMetaCallReturn
type RecursiveMetaCallReturn = RecursiveMetaCall | RecursiveMetaCallReturn # error: [cyclic-type-alias-definition]
class RecursiveMeta(type):
def __call__(cls, value: int) -> RecursiveMetaCallReturn:
@@ -186,7 +186,7 @@ class G:
def __call__(self, key: int) -> "RecursiveGetItem":
return self
type RecursiveGetItem = G | RecursiveGetItem
type RecursiveGetItem = G | RecursiveGetItem # error: [cyclic-type-alias-definition]
class C:
__getitem__: RecursiveGetItem = G()
@@ -45,10 +45,8 @@ from typing import Union, TypeAliasType, Sequence, Mapping
A = list["A | None"]
def f(x: A):
# TODO: should be `list[A | None]`?
reveal_type(x) # revealed: list[Divergent]
# TODO: should be `A | None`?
reveal_type(x[0]) # revealed: Divergent
reveal_type(x) # revealed: list[A | None]
reveal_type(x[0]) # revealed: list[A | None] | None
JSONPrimitive = Union[str, int, float, bool, None]
JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]')
@@ -172,7 +170,7 @@ from typing import reveal_type
class G:
pass
type RecursiveAlias = G | RecursiveAlias
type RecursiveAlias = G | RecursiveAlias # error: [cyclic-type-alias-definition]
class C:
x: RecursiveAlias
@@ -81,7 +81,8 @@ def f(x: Any, y: Unknown, z: Any | str | int):
e = cast(str | int | Any, z) # error: [redundant-cast]
```
Recursive aliases that fall back to `Divergent` should not trigger `redundant-cast`.
Recursive aliases preserve their self-references when possible, so equivalent casts are still
redundant.
```toml
[environment]
@@ -94,7 +95,16 @@ from typing import cast
RecursiveAlias = list["RecursiveAlias | None"]
def f(x: RecursiveAlias):
cast(RecursiveAlias, x)
cast(RecursiveAlias, x) # error: [redundant-cast]
```
Stringified type expressions in non-alias assignments should not preserve self-references as
recursive aliases.
```py
from typing import cast
x = cast("str | x", "") # error: [invalid-type-form]
```
## Diagnostic snapshots
@@ -1667,7 +1667,7 @@ from typing import Union
Recursive = list[Union["Recursive", None]]
def _(r: Recursive):
reveal_type(r) # revealed: list[Divergent]
reveal_type(r) # revealed: list[Recursive | None]
```
### New union syntax
@@ -1695,12 +1695,27 @@ def _(
recursive_dict3: RecursiveDict3,
recursive_dict4: RecursiveDict4,
):
reveal_type(recursive_list1) # revealed: list[Divergent]
reveal_type(recursive_list2) # revealed: list[Divergent]
reveal_type(recursive_dict1) # revealed: dict[str, Divergent]
reveal_type(recursive_dict2) # revealed: dict[str, Divergent]
reveal_type(recursive_dict3) # revealed: dict[Divergent, int]
reveal_type(recursive_dict4) # revealed: dict[Divergent, int]
reveal_type(recursive_list1) # revealed: list[RecursiveList1 | None]
reveal_type(recursive_list2) # revealed: list[RecursiveList2 | None]
reveal_type(recursive_dict1) # revealed: dict[str, RecursiveDict1 | None]
reveal_type(recursive_dict2) # revealed: dict[str, RecursiveDict2 | None]
reveal_type(recursive_dict3) # revealed: dict[RecursiveDict3, int]
reveal_type(recursive_dict4) # revealed: dict[RecursiveDict4, int]
```
### Mutually recursive implicit type aliases
```py
A = list["B"]
B = list["A"]
C = list["D"]
D = int
def _(a: A, b: B, c: C):
reveal_type(a) # revealed: list[B]
reveal_type(b) # revealed: list[A]
reveal_type(c) # revealed: list[int]
```
### Self-referential generic implicit type aliases
@@ -412,11 +412,11 @@ def _(flag: bool):
def _(flag: bool):
x = 1 if flag else "a"
# error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Divergent, ...]`, found `Literal["a"]"
# error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[_ClassInfo, ...]`, found `Literal["a"]"
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1, "a"]
# error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Divergent, ...]`, found `Literal["int"]"
# error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[_ClassInfo, ...]`, found `Literal["int"]"
if isinstance(x, "int"):
reveal_type(x) # revealed: Literal[1, "a"]
```
@@ -429,12 +429,11 @@ def flag() -> bool:
t = int if flag() else str
# error: [invalid-argument-type] "Argument to function `issubclass` is incorrect: Expected `type | UnionType | tuple[Divergent, ...]`, found `Literal["str"]"
# error: [invalid-argument-type] "Argument to function `issubclass` is incorrect: Expected `type | UnionType | tuple[_ClassInfo, ...]`, found `Literal["str"]"
if issubclass(t, "str"):
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
# TODO: this should cause us to emit a diagnostic during
# type checking
# error: [invalid-argument-type] "Argument to function `issubclass` is incorrect: Expected `type | UnionType | tuple[_ClassInfo, ...]`, found `tuple[<class 'bytes'>, Literal["str"]]`"
if issubclass(t, (bytes, "str")):
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
@@ -311,23 +311,23 @@ from types import UnionType
RecursiveTuple: TypeAlias = tuple["int | RecursiveTuple", str]
def _(rec: RecursiveTuple):
# TODO should be `tuple[int | RecursiveTuple, str]`
reveal_type(rec) # revealed: tuple[Divergent, str]
reveal_type(rec) # revealed: tuple[int | RecursiveTuple, str]
RecursiveHomogeneousTuple: TypeAlias = tuple["int | RecursiveHomogeneousTuple", ...]
def _(rec: RecursiveHomogeneousTuple):
# TODO should be `tuple[int | RecursiveHomogeneousTuple, ...]`
reveal_type(rec) # revealed: tuple[Divergent, ...]
reveal_type(rec) # revealed: tuple[int | RecursiveHomogeneousTuple, ...]
ClassInfo: TypeAlias = type | UnionType | tuple["ClassInfo", ...]
reveal_type(ClassInfo) # revealed: <types.UnionType special-form 'type | UnionType | tuple[Divergent, ...]'>
reveal_type(ClassInfo) # revealed: <types.UnionType special-form 'type | UnionType | tuple[ClassInfo, ...]'>
def my_isinstance(obj: object, classinfo: ClassInfo) -> bool:
# TODO should be `type | UnionType | tuple[ClassInfo, ...]`
reveal_type(classinfo) # revealed: type | UnionType | tuple[Divergent, ...]
reveal_type(classinfo) # revealed: type | UnionType | tuple[ClassInfo, ...]
return isinstance(obj, classinfo)
Itself: TypeAlias = "Itself" # error: [cyclic-type-alias-definition]
RedundantUnion: TypeAlias = "int | RedundantUnion" # error: [cyclic-type-alias-definition]
K = TypeVar("K")
V = TypeVar("V")
NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]]
@@ -343,7 +343,7 @@ my_isinstance(1, (int, (str, float)))
my_isinstance(1, (int, (str | float)))
# error: [invalid-argument-type]
my_isinstance(1, 1)
# TODO should be an invalid-argument-type error
# error: [invalid-argument-type]
my_isinstance(1, (int, (str, 1)))
```
@@ -392,6 +392,7 @@ deferred in a stub file, allowing for forward references:
from typing import TypeAlias
MyAlias: TypeAlias = A | B
Recursive: TypeAlias = list[Recursive]
class A: ...
class B: ...
@@ -407,6 +408,9 @@ def f(x: stub.MyAlias): ...
f(stub.A())
f(stub.B())
def g(x: stub.Recursive):
reveal_type(x) # revealed: list[Recursive]
class Unrelated: ...
# error: [invalid-argument-type]
@@ -402,8 +402,9 @@ def g(x: RecursiveList):
### Invalid self-referential
```py
# TODO emit a diagnostic on these two lines
# error: [cyclic-type-alias-definition] "Cyclic definition of `IntOr`"
type IntOr = int | IntOr
# error: [cyclic-type-alias-definition] "Cyclic definition of `OrInt`"
type OrInt = OrInt | int
def f(x: IntOr, y: OrInt):
@@ -447,8 +448,9 @@ type I[T] = H[T]
# It's not possible to create an element of this type, but it's not an error for now
type DirectRecursiveList[T] = list[DirectRecursiveList[T]]
# TODO: this should probably be a cyclic-type-alias-definition error
# error: [cyclic-type-alias-definition] "Cyclic definition of `Foo`"
type Foo[T] = list[T] | Bar[T]
# error: [cyclic-type-alias-definition] "Cyclic definition of `Bar`"
type Bar[T] = int | Foo[T]
def _(x: Bar[int]):
@@ -463,7 +465,7 @@ from ty_extensions import Intersection, Not, all_members, has_member
class RecursiveItem: ...
type RecursiveUnion = RecursiveItem | RecursiveUnion
type RecursiveUnion = RecursiveItem | RecursiveUnion # error: [cyclic-type-alias-definition]
def subscript_recursive_alias(x: RecursiveUnion):
# error: [not-subscriptable] "Cannot subscript object of type `RecursiveItem` with no `__getitem__` method"
@@ -473,7 +475,7 @@ class RecursiveIterableItem:
def __iter__(self) -> Iterator["RecursiveIterable"]:
return iter([self])
type RecursiveIterable = RecursiveIterableItem | RecursiveIterable
type RecursiveIterable = RecursiveIterableItem | RecursiveIterable # error: [cyclic-type-alias-definition]
def iterate_recursive_alias(x: RecursiveIterable):
for y in x:
@@ -488,7 +490,7 @@ class RecursiveCallable:
def __call__(self) -> "DunderRecursive":
return self
type DunderRecursive = RecursiveCallable | DunderRecursive
type DunderRecursive = RecursiveCallable | DunderRecursive # error: [cyclic-type-alias-definition]
class HasRecursiveLen:
__len__: DunderRecursive = RecursiveCallable()
@@ -500,7 +502,7 @@ def len_recursive_alias(x: HasRecursiveLen):
class RecursiveMembers:
attr: int
type RecursiveMembersAlias = RecursiveMembers | RecursiveMembersAlias
type RecursiveMembersAlias = RecursiveMembers | RecursiveMembersAlias # error: [cyclic-type-alias-definition]
def list_recursive_alias_members(x: RecursiveMembersAlias):
all_members(x)
@@ -509,7 +511,7 @@ def list_recursive_alias_members(x: RecursiveMembersAlias):
class RecursiveAttribute:
attr: int
type RecursiveAttributeAlias = RecursiveAttribute | RecursiveAttributeAlias
type RecursiveAttributeAlias = RecursiveAttribute | RecursiveAttributeAlias # error: [cyclic-type-alias-definition]
def assign_recursive_alias_attribute(x: RecursiveAttributeAlias):
x.attr = 1
@@ -519,6 +521,7 @@ def delete_recursive_alias_attribute(x: RecursiveAttributeAlias):
class RecursiveMissingAttribute: ...
# error: [cyclic-type-alias-definition]
type RecursiveMissingAttributeAlias = RecursiveMissingAttribute | RecursiveMissingAttributeAlias
def load_missing_recursive_alias_attribute(x: RecursiveMissingAttributeAlias):
@@ -529,7 +532,7 @@ def delete_missing_recursive_alias_attribute(x: RecursiveMissingAttributeAlias):
# error: [unresolved-attribute] "Attribute `missing` is not defined on `RecursiveMissingAttribute` in union `RecursiveMissingAttributeAlias`"
del x.missing
type RecursiveIntAttributeAlias = int | RecursiveIntAttributeAlias
type RecursiveIntAttributeAlias = int | RecursiveIntAttributeAlias # error: [cyclic-type-alias-definition]
def load_missing_recursive_int_alias_attribute(x: RecursiveIntAttributeAlias):
# error: [unresolved-attribute] "Attribute `missing` is not defined on `int` in union `RecursiveIntAttributeAlias`"
@@ -539,7 +542,7 @@ class RecursiveKwargs(TypedDict):
kind: Literal["a"]
a: int
type RecursiveKwargsAlias = RecursiveKwargs | RecursiveKwargsAlias
type RecursiveKwargsAlias = RecursiveKwargs | RecursiveKwargsAlias # error: [cyclic-type-alias-definition]
def call_with_recursive_kwargs_alias(x: RecursiveKwargsAlias):
def g(a: int): ...
@@ -550,7 +553,7 @@ def narrow_recursive_typed_dict_alias(x: RecursiveKwargsAlias):
if x["kind"] == "a":
reveal_type(x) # revealed: RecursiveKwargs
type RecursiveDecoratorReturn = Callable[[int], int] | RecursiveDecoratorReturn
type RecursiveDecoratorReturn = Callable[[int], int] | RecursiveDecoratorReturn # error: [cyclic-type-alias-definition]
def recursive_decorator_return(fn: Callable[[int], int]) -> RecursiveDecoratorReturn: ...
@recursive_decorator_return
@@ -639,7 +642,7 @@ class BaseWithMethod:
class RecursiveSuperOwner(BaseWithMethod): ...
type RecursiveSuperOwnerAlias = RecursiveSuperOwner | RecursiveSuperOwnerAlias
type RecursiveSuperOwnerAlias = RecursiveSuperOwner | RecursiveSuperOwnerAlias # error: [cyclic-type-alias-definition]
def recursive_super_owner(x: RecursiveSuperOwnerAlias):
super(RecursiveSuperOwner, x).method()
@@ -668,7 +671,7 @@ def expand_recursive_variadic_tuple_argument(x: RecursiveTuple):
# error: [invalid-argument-type] "Argument to function `overloaded_recursive_tuple_variadic` is incorrect: Expected `int`, found `RecursiveTuple`"
reveal_type(overloaded_recursive_tuple_variadic(*x)) # revealed: Unknown
type RecursiveClassInfo = type[int] | RecursiveClassInfo
type RecursiveClassInfo = type[int] | RecursiveClassInfo # error: [cyclic-type-alias-definition]
def narrow_recursive_classinfo(x: object, y: RecursiveClassInfo):
if isinstance(x, y):
@@ -764,6 +767,11 @@ static_assert(is_subtype_of(Top[JsonDict], Top[JsonDict]))
static_assert(is_subtype_of(Top[JsonDict], Bottom[JsonDict]))
static_assert(is_subtype_of(Bottom[JsonDict], Bottom[JsonDict]))
static_assert(is_subtype_of(Bottom[JsonDict], Top[JsonDict]))
type MutuallySpecializingA[T] = list[MutuallySpecializingB[list[T]]]
type MutuallySpecializingB[T] = tuple[MutuallySpecializingA[T]]
static_assert(is_subtype_of(Bottom[MutuallySpecializingA[int]], Top[MutuallySpecializingA[int]]))
```
### Equivalence of top materializations of mutually recursive invariant aliases
@@ -948,7 +956,7 @@ x: Literal[RecursiveLiteralSpecialization[int]]
```py
from typing import reveal_type
type A = int | A
type A = int | A # error: [cyclic-type-alias-definition]
def foo(x: A):
reveal_type(x + 1) # revealed: int
@@ -1866,6 +1866,8 @@ def _(
# No error here:
reveal_type(person[unknown_key]) # revealed: Unknown
# error: [invalid-key]
# error: [invalid-key]
reveal_type(movie[recursive_key[0]]) # revealed: Unknown
# error: [invalid-key] "Unknown key "anything" for TypedDict `Animal`"
+34 -7
View File
@@ -1378,13 +1378,6 @@ impl<'db> Type<'db> {
any_over_type(db, self, false, |ty| ty.is_dynamic())
}
pub(crate) const fn as_special_form(self) -> Option<SpecialFormType> {
match self {
Type::SpecialForm(special_form) => Some(special_form),
_ => None,
}
}
pub const fn as_property_instance(self) -> Option<PropertyInstanceType<'db>> {
match self {
Type::PropertyInstance(property) => Some(property),
@@ -1421,6 +1414,10 @@ impl<'db> Type<'db> {
///
/// The caller owns the fallback result because different operations need different conservative
/// answers when a recursive alias points back to itself.
///
/// ```python
/// type RecursiveList = list[RecursiveList]
/// ```
pub(crate) fn visit_type_alias_value<R>(
self,
db: &'db dyn Db,
@@ -1438,6 +1435,10 @@ impl<'db> Type<'db> {
///
/// This variant returns `R::default()` when a recursive alias points back to itself. Use this
/// for predicate and extraction operations where "not available" is the conservative answer.
///
/// ```python
/// type RecursiveList = list[RecursiveList]
/// ```
pub(crate) fn visit_type_alias_value_or_default<R: Default>(
self,
db: &'db dyn Db,
@@ -1451,6 +1452,10 @@ impl<'db> Type<'db> {
/// This variant returns `true` when a recursive alias points back to itself. Use this for
/// validation operations where emitting an error from an unexpanded recursive component would
/// be less accurate than accepting the operation.
///
/// ```python
/// type RecursiveList = list[RecursiveList]
/// ```
pub(crate) fn visit_type_alias_value_or_assume_valid(
self,
db: &'db dyn Db,
@@ -1473,6 +1478,10 @@ impl<'db> Type<'db> {
///
/// Unlike [`Type::as_union_like`], this keeps active type-alias recursion state alive while
/// visiting the elements. Use this for recursive walks over alias-backed unions.
///
/// ```python
/// type Json = None | bool | int | str | list[Json] | dict[str, Json]
/// ```
pub(crate) fn visit_union_like_elements(
self,
db: &'db dyn Db,
@@ -3795,6 +3804,18 @@ impl<'db> Type<'db> {
/// In the second case, the return type of `len()` in `typeshed` (`int`)
/// is used as a fallback.
fn len(&self, db: &'db dyn Db) -> Option<Type<'db>> {
/// Extracts a non-negative integer literal from a type returned by `__len__`.
///
/// This also looks through alias-backed return types, such as:
///
/// ```python
/// from typing import Literal
///
/// type Length = Literal[3]
///
/// class Sized:
/// def __len__(self) -> Length: ...
/// ```
fn non_negative_int_literal_impl<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Type<'db>> {
match ty {
// TODO: Emit diagnostic for non-integers and negative integers
@@ -4349,6 +4370,12 @@ impl<'db> Type<'db> {
}
/// Returns dynamic call bindings used as the conservative fallback for recursive callability.
///
/// ```python
/// from collections.abc import Callable
///
/// type RecursiveCallable = Callable[[RecursiveCallable], int]
/// ```
fn unknown_bindings() -> Bindings<'db> {
let unknown = Type::unknown();
Binding::single(unknown, Signature::dynamic(unknown)).into()
+150 -10
View File
@@ -4511,6 +4511,100 @@ fn validate_keyword_unpack_key_type<'db>(
}
}
fn is_recursive_classinfo_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
match ty {
Type::TypeAlias(alias) => is_recursive_classinfo_type(db, alias.value_type(db)),
Type::Union(union) => {
let mut has_type = false;
let mut has_union_type = false;
let mut has_recursive_tuple = false;
for element in union.elements(db) {
match element {
Type::NominalInstance(instance)
if instance.has_known_class(db, KnownClass::Type) =>
{
has_type = true;
}
Type::NominalInstance(instance)
if instance.has_known_class(db, KnownClass::UnionType) =>
{
has_union_type = true;
}
Type::NominalInstance(instance)
if instance.tuple_spec(db).is_some_and(|tuple| {
tuple
.variable_element()
.is_some_and(|element| matches!(*element, Type::TypeAlias(_)))
}) =>
{
has_recursive_tuple = true;
}
_ => return false,
}
}
has_type && has_union_type && has_recursive_tuple
}
_ => false,
}
}
fn known_classinfo_function<'db>(db: &'db dyn Db, callable_ty: Type<'db>) -> Option<KnownFunction> {
callable_ty
.as_function_literal()
.and_then(|function| function.known(db))
.filter(|function| {
matches!(
function,
KnownFunction::IsInstance | KnownFunction::IsSubclass
)
})
}
fn is_valid_runtime_classinfo_argument<'db>(
db: &'db dyn Db,
function: Option<KnownFunction>,
ty: Type<'db>,
) -> bool {
if is_recursive_classinfo_type(db, ty) {
return true;
}
match ty {
Type::ClassLiteral(_) | Type::SubclassOf(_) => true,
Type::SpecialForm(special_form)
if special_form.is_valid_isinstance_target()
|| (function == Some(KnownFunction::IsSubclass)
&& special_form == SpecialFormType::Any) =>
{
true
}
Type::KnownInstance(KnownInstanceType::UnionType(instance)) => {
instance.value_expression_types(db).is_ok_and(|elements| {
elements.into_iter().all(|element| {
element.is_none(db)
|| is_valid_runtime_classinfo_argument(db, function, element)
})
})
}
Type::NominalInstance(instance) => instance.tuple_spec(db).is_some_and(|tuple| {
tuple
.iter_all_elements()
.all(|element| is_valid_runtime_classinfo_argument(db, function, element))
}),
Type::Union(union) => union
.elements(db)
.iter()
.all(|element| is_valid_runtime_classinfo_argument(db, function, *element)),
Type::TypeAlias(_) => ty.visit_type_alias_value_or_assume_valid(db, |value_ty| {
is_valid_runtime_classinfo_argument(db, function, value_ty)
}),
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => true,
_ => false,
}
}
impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
#[expect(clippy::too_many_arguments)]
fn new(
@@ -4881,11 +4975,25 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
// building them in an earlier separate step.
//
// TODO: handle starred annotations, e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...]]`
let assignability = argument_type.when_assignable_to(
self.db,
expected_ty,
constraints,
self.inferable_typevars,
);
let known_classinfo_function = known_classinfo_function(self.db, self.signature_type);
let invalid_classinfo_argument = known_classinfo_function.is_none()
&& assignability.is_always_satisfied(self.db)
&& is_recursive_classinfo_type(self.db, expected_ty)
&& !is_valid_runtime_classinfo_argument(
self.db,
known_classinfo_function,
argument_type,
);
if !self.constraint_set_errors[argument_index]
&& !parameter.has_starred_annotation()
&& argument_type
.when_assignable_to(self.db, expected_ty, constraints, self.inferable_typevars)
.is_never_satisfied(self.db)
&& (assignability.is_never_satisfied(self.db) || invalid_classinfo_argument)
{
let positional = matches!(argument, Argument::Positional | Argument::Synthetic)
&& !parameter.is_variadic();
@@ -6238,6 +6346,34 @@ fn invalid_dataclass_target<'db>(
}
impl<'db> BindingError<'db> {
fn is_valid_classinfo_with_special_form(
db: &'db dyn Db,
function: KnownFunction,
ty: Type<'db>,
) -> Option<bool> {
match ty {
Type::ClassLiteral(_) => Some(false),
Type::SpecialForm(special_form)
if special_form.is_valid_isinstance_target()
|| (function == KnownFunction::IsSubclass
&& special_form == SpecialFormType::Any) =>
{
Some(true)
}
Type::KnownInstance(KnownInstanceType::UnionType(_)) => Some(false),
Type::NominalInstance(nominal) => {
let tuple = nominal.tuple_spec(db)?;
tuple
.iter_all_elements()
.try_fold(false, |contains, element| {
Self::is_valid_classinfo_with_special_form(db, function, element)
.map(|element_contains| contains || element_contains)
})
}
_ => None,
}
}
/// Returns `true` if this error indicates the overload didn't match the call arguments.
///
/// Returns `false` for semantic errors where the overload matched the types but the
@@ -6300,15 +6436,19 @@ impl<'db> BindingError<'db> {
// error-prone, due to the fact that they are annotated with recursive type aliases.
if parameter.index == 1
&& *argument_index == Some(1)
&& let Some(function) = callable_ty
.as_function_literal()
.and_then(|function| function.known(context.db()))
&& matches!(
callable_ty
.as_function_literal()
.and_then(|function| function.known(context.db())),
Some(KnownFunction::IsInstance | KnownFunction::IsSubclass)
function,
KnownFunction::IsInstance | KnownFunction::IsSubclass
)
&& provided_ty
.as_special_form()
.is_some_and(SpecialFormType::is_valid_isinstance_target)
&& Self::is_valid_classinfo_with_special_form(
context.db(),
function,
*provided_ty,
)
.is_some_and(|contains_special_form| contains_special_form)
{
return;
}
@@ -52,9 +52,19 @@ pub(crate) type PairVisitor<'db, Tag, C> = CycleDetector<Tag, (Type<'db>, Type<'
enum TypeAliasKey {
PEP695(salsa::Id),
ManualPEP695(salsa::Id),
Legacy(salsa::Id),
}
impl TypeAliasKey {
/// Converts a type alias into an active-recursion key that preserves the alias kind.
///
/// Distinguishing the alias kind keeps unrelated aliases with the same Salsa id space from
/// colliding when expanding definitions such as:
///
/// ```python
/// type PEP695 = list[PEP695]
/// Legacy = list["Legacy"]
/// ```
fn from_type_alias(db: &dyn Db, type_alias: TypeAliasType<'_>) -> Self {
match type_alias {
TypeAliasType::PEP695(type_alias) => {
@@ -63,6 +73,7 @@ impl TypeAliasKey {
TypeAliasType::ManualPEP695(type_alias) => {
TypeAliasKey::ManualPEP695(type_alias.definition(db).as_id())
}
TypeAliasType::Legacy(type_alias) => TypeAliasKey::Legacy(type_alias.as_id()),
}
}
}
@@ -98,6 +109,15 @@ impl TypeAliasRecursionVisitor {
}
}
/// Runs `func` while recording that a type alias is actively being expanded.
///
/// If the same alias is encountered again before `func` returns, `on_cycle` supplies the
/// conservative result for that recursive edge. This keeps operations over aliases productive for
/// definitions such as:
///
/// ```python
/// type Json = None | bool | int | str | list[Json] | dict[str, Json]
/// ```
pub(crate) fn visit_type_alias<R>(
db: &dyn Db,
type_alias: TypeAliasType<'_>,
@@ -269,6 +289,7 @@ struct ActiveRecursionGuard<'a, T: Hash + Eq> {
}
impl<T: Hash + Eq> Drop for ActiveRecursionGuard<'_, T> {
/// Removes the active recursion key when the guarded expansion scope exits.
fn drop(&mut self) {
self.seen.borrow_mut().remove(self.item);
}
@@ -20,7 +20,9 @@ use crate::types::relation::{
};
use crate::types::signatures::{CallableSignature, Parameters, SignatureRelationVisitor};
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
use crate::types::type_alias::{walk_manual_pep_695_type_alias, walk_pep_695_type_alias};
use crate::types::type_alias::{
walk_legacy_type_alias, walk_manual_pep_695_type_alias, walk_pep_695_type_alias,
};
use crate::types::typevar::{
BoundTypeVarIdentity, TypeVarIdentity, TypeVarInstance, walk_type_var_bounds,
};
@@ -738,6 +740,9 @@ impl<'db> GenericContext<'db> {
TypeAliasType::ManualPEP695(type_alias) => {
walk_manual_pep_695_type_alias(db, type_alias, self);
}
TypeAliasType::Legacy(type_alias) => {
walk_legacy_type_alias(db, type_alias, self);
}
}
}
@@ -1300,6 +1300,9 @@ bitflags::bitflags! {
/// Whether we're in a context where `Unpack` can be legal.
const IN_VALID_UNPACK_CONTEXT = 1 << 10;
/// Whether the visitor is currently visiting an assignment-based type alias value.
const IN_LEGACY_TYPE_ALIAS_VALUE = 1 << 11;
/// Whether the visitor is currently visiting a type expression.
const IN_TYPE_EXPRESSION = 1 << 12;
@@ -71,7 +71,9 @@ use crate::types::enums::{enum_ignored_names, is_enum_class_by_inheritance};
use crate::types::function::{
FunctionDecorators, FunctionType, KnownFunction, report_revealed_type,
};
use crate::types::generics::{InferableTypeVars, SpecializationBuilder, bind_typevar};
use crate::types::generics::{
InferableTypeVars, Specialization, SpecializationBuilder, bind_typevar,
};
use crate::types::infer::builder::named_tuple::NamedTupleKind;
use crate::types::infer::builder::paramspec_validation::validate_paramspec_components;
use crate::types::infer::builder::typed_dict::TypedDictConstructorForm;
@@ -87,7 +89,9 @@ use crate::types::special_form::TypeQualifier;
use crate::types::subclass_of::SubclassOfInner;
use crate::types::tuple::promotion::TupleSizePromotionConstraints;
use crate::types::tuple::{Tuple, TupleLength, TupleSpecBuilder, TupleType};
use crate::types::type_alias::{ManualPEP695TypeAliasType, PEP695TypeAliasType};
use crate::types::type_alias::{
LegacyTypeAliasType, ManualPEP695TypeAliasType, PEP695TypeAliasType,
};
use crate::types::typevar::{BoundTypeVarIdentity, TypeVarConstraints, TypeVarIdentity};
use crate::types::{
CallDunderError, CallableBinding, CallableType, CallableTypes, ClassType, DynamicType,
@@ -96,8 +100,8 @@ use crate::types::{
ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType,
SubclassOfType, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, TypedDictType, UnionAccumulator,
UnionBuilder, UnionType, binding_type, infer_complete_scope_types, infer_scope_types,
todo_type,
UnionBuilder, UnionType, any_over_type, binding_type, infer_complete_scope_types,
infer_scope_types, todo_type,
};
use crate::{AnalysisSettings, Db, FxIndexSet, Program};
use ty_python_core::ast_ids::ScopedUseId;
@@ -1443,37 +1447,186 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
previous_check_unbound_typevars,
);
// A type alias where a value type points to itself, i.e. the expanded type is `Divergent` is meaningless
// (but a type alias that expands to something like `list[Divergent]` may be a valid recursive type alias)
// and would lead to infinite recursion. Therefore, such type aliases should not be exposed.
// A type alias where a value type points to itself without passing through a productive
// type constructor is meaningless and would lead to infinite recursion. Therefore, such
// type aliases should not be exposed.
// ```python
// type Itself = Itself # error: "Cyclic definition of `Itself`"
// type A = B # error: "Cyclic definition of `A`"
// type B = A # error: "Cyclic definition of `B`"
// type G[T] = G[T] # error: "Cyclic definition of `G`"
// type IntOr = int | IntOr # error: "Cyclic definition of `IntOr`"
// type RecursiveList[T] = list[T | RecursiveList[T]] # OK
// type RecursiveList2[T] = list[RecursiveList2[T]] # It's not possible to create an element of this, but it's not an error for now
// type IntOr = int | IntOr # It's redundant, but OK for now
// type IntOrStr = int | StrOrInt # It's redundant, but OK
// type StrOrInt = str | IntOrStr # It's redundant, but OK
// ```
let expanded = value_ty.expand_eagerly(self.db());
let definition = self.index.expect_single_definition(type_alias);
let Some(expanded) =
self.cyclic_type_alias_expansion(value_ty, definition, &type_alias.value)
else {
return;
};
if let Some(alias_name) = type_alias.name.as_name_expr() {
self.report_cyclic_type_alias_definition(type_alias, &alias_name.id);
}
// Replace pure alias cycles with `Divergent`. For cycles that were erased by union
// expansion, keep the simplified type to preserve downstream analysis.
if expanded.is_divergent() {
if let Some(builder) = self
.context
.report_lint(&CYCLIC_TYPE_ALIAS_DEFINITION, type_alias)
{
builder.into_diagnostic(format_args!(
"Cyclic definition of `{}`",
&type_alias.name.as_name_expr().unwrap().id,
));
}
// Replace with `Divergent`.
self.expressions
.insert(type_alias.value.as_ref().into(), expanded);
}
}
/// Returns the eagerly expanded value when a type alias has a cyclic definition.
///
/// Pure alias cycles are reported and replaced with `Divergent`, while productive recursive
/// aliases are allowed to keep their alias identity:
///
/// ```python
/// type A = B
/// type B = A
/// type RecursiveList = list[RecursiveList]
/// ```
fn cyclic_type_alias_expansion(
&self,
value_ty: Type<'db>,
definition: Definition<'db>,
value: &ast::Expr,
) -> Option<Type<'db>> {
let expanded = value_ty.expand_eagerly(self.db());
let has_unproductive_cycle = !self.has_parse_error(value)
&& self.has_unproductive_type_alias_cycle(value_ty, definition);
(expanded.is_divergent() || has_unproductive_cycle).then_some(expanded)
}
/// Emits the `cyclic-type-alias-definition` diagnostic for a single alias target.
///
/// ```python
/// type A = A
/// ```
fn report_cyclic_type_alias_definition(&self, target: impl Ranged, alias_name: &str) {
if let Some(builder) = self
.context
.report_lint(&CYCLIC_TYPE_ALIAS_DEFINITION, target)
{
builder.into_diagnostic(format_args!("Cyclic definition of `{alias_name}`"));
}
}
/// Returns `true` if a type contains an unproductive reference back to the current alias.
///
/// This detects alias-only cycles that can be hidden inside unions after eager expansion:
///
/// ```python
/// type IntOr = int | IntOr
/// ```
fn has_unproductive_type_alias_cycle(
&self,
ty: Type<'db>,
current_definition: Definition<'db>,
) -> bool {
self.has_unproductive_type_alias_cycle_impl(
ty,
current_definition,
&mut FxHashSet::default(),
&mut Vec::new(),
)
}
fn is_unproductive_self_alias_reference(
&self,
alias: TypeAliasType<'db>,
active_specializations: &[Specialization<'db>],
) -> bool {
let Some(generic_context) = alias.generic_context(self.db()) else {
return true;
};
let Some(specialization) = alias.specialization(self.db()) else {
return true;
};
let normalized_specialization = active_specializations.iter().rev().fold(
specialization,
|specialization, active_specialization| {
specialization.apply_specialization(self.db(), *active_specialization)
},
);
normalized_specialization == generic_context.identity_specialization(self.db())
}
/// Recursively searches a type for an unproductive reference to `current_definition`.
///
/// `seen_aliases` prevents mutually recursive aliases from causing this structural walk to
/// loop while checking definitions such as:
///
/// ```python
/// type A = B
/// type B = A
/// ```
fn has_unproductive_type_alias_cycle_impl(
&self,
ty: Type<'db>,
current_definition: Definition<'db>,
seen_aliases: &mut FxHashSet<Definition<'db>>,
active_specializations: &mut Vec<Specialization<'db>>,
) -> bool {
match ty {
Type::Union(union) => union.elements(self.db()).iter().any(|element| {
self.has_unproductive_type_alias_cycle_impl(
*element,
current_definition,
seen_aliases,
active_specializations,
)
}),
Type::TypeAlias(alias) => {
let definition = alias.definition(self.db());
if definition == current_definition {
return self
.is_unproductive_self_alias_reference(alias, active_specializations);
}
if !seen_aliases.insert(definition) {
return false;
}
let specialization_count = active_specializations.len();
if let Some(specialization) = alias.specialization(self.db()) {
active_specializations.push(specialization);
}
let has_cycle = self.has_unproductive_type_alias_cycle_impl(
alias.raw_value_type(self.db()),
current_definition,
seen_aliases,
active_specializations,
);
active_specializations.truncate(specialization_count);
seen_aliases.remove(&definition);
has_cycle
}
_ => false,
}
}
/// Returns `true` if `expression` contains a parse error.
///
/// This suppresses secondary cyclic-alias diagnostics when the parser has already reported an
/// incomplete value, for example:
///
/// ```python
/// type Bad = list[
/// ```
fn has_parse_error(&self, expression: &ast::Expr) -> bool {
self.module()
.errors()
.iter()
.any(|error| expression.range().contains_range(error.range()))
}
/// If the current scope is a method inside an enclosing class,
/// return `Some(class)` where `class` represents the enclosing class.
///
@@ -3368,6 +3521,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// order to bind `T` to `OptionalList`.
let previous_typevar_binding_context =
self.typevar_binding_context.replace(definition);
let previous_in_legacy_type_alias_value = self.context.inference_flags.replace(
InferenceFlags::IN_LEGACY_TYPE_ALIAS_VALUE,
Self::is_legacy_type_alias_value_candidate(value),
);
let value_ty = if let Some(standalone_expression) = self.index.try_expression(value)
{
@@ -3448,6 +3605,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_expression(value, tcx)
};
self.context.inference_flags.set(
InferenceFlags::IN_LEGACY_TYPE_ALIAS_VALUE,
previous_in_legacy_type_alias_value,
);
self.typevar_binding_context = previous_typevar_binding_context;
// `TYPE_CHECKING` is a special variable that should only be assigned `False`
@@ -4208,12 +4369,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// the definition of `OptionalList` as the binding context while inferring the
// RHS (`list[T] | None`), in order to bind `T` to `OptionalList`.
let previous_typevar_binding_context = self.typevar_binding_context.replace(definition);
let previous_in_legacy_type_alias_value = self.context.inference_flags.replace(
InferenceFlags::IN_LEGACY_TYPE_ALIAS_VALUE,
is_pep_613_type_alias,
);
let inferred_ty = self.infer_maybe_standalone_expression(
value,
TypeContext::new(Some(declared.inner_type())),
);
self.context.inference_flags.set(
InferenceFlags::IN_LEGACY_TYPE_ALIAS_VALUE,
previous_in_legacy_type_alias_value,
);
self.typevar_binding_context = previous_typevar_binding_context;
self.deferred_state = previous_deferred_state;
self.dataclass_field_specifiers.clear();
@@ -4233,6 +4402,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
};
if is_pep_613_type_alias {
let alias_value_ty = inferred_ty
.default_specialize(self.db())
.in_type_expression(
self.db(),
self.scope(),
Some(definition),
self.inference_flags(),
)
.unwrap_or_else(|_| Type::unknown());
if self
.cyclic_type_alias_expansion(alias_value_ty, definition, value)
.is_some()
&& let Some(target_name) = target.as_name_expr()
{
self.report_cyclic_type_alias_definition(target, &target_name.id);
}
let inferred_ty =
if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = inferred_ty {
let identity = TypeVarIdentity::new(
@@ -7836,6 +8023,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
id: symbol_name,
ctx: _,
} = name_node;
let expr = PlaceExpr::from_expr_name(name_node);
let db = self.db();
@@ -7895,6 +8083,221 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ty.inner_type()
}
/// Preserves a direct self-reference inside a deferred assignment-based alias value.
///
/// This covers both stub-file aliases and stringized values without inspecting strings
/// directly:
///
/// ```python
/// from typing import TypeAlias
///
/// A: TypeAlias = list[A]
/// ```
fn self_referential_legacy_type_alias(
&self,
name_node: &ast::ExprName,
) -> Option<TypeAliasType<'db>> {
if !self.is_deferred()
|| !self
.inference_flags()
.contains(InferenceFlags::IN_LEGACY_TYPE_ALIAS_VALUE)
{
return None;
}
let (current_definition, target_name) = self.current_legacy_type_alias_definition()?;
if target_name.id != name_node.id {
return None;
}
Some(TypeAliasType::Legacy(LegacyTypeAliasType::new(
self.db(),
&name_node.id,
current_definition,
)))
}
/// Preserves a reference to another legacy alias when normal inference exposes a cycle.
///
/// The reference is first inferred normally; only if that result contains `Divergent` do we
/// recover the referenced assignment-based alias identity. This handles mutual recursion such
/// as:
///
/// ```python
/// A = list["B"]
/// B = list["A"]
/// ```
fn divergent_legacy_type_alias_reference(
&self,
name_node: &ast::ExprName,
inferred_ty: Type<'db>,
) -> Option<TypeAliasType<'db>> {
if !self.is_deferred()
|| !self
.inference_flags()
.contains(InferenceFlags::IN_LEGACY_TYPE_ALIAS_VALUE)
|| !any_over_type(self.db(), inferred_ty, true, |ty| ty.is_divergent())
{
return None;
}
let (current_definition, target_name) = self.current_legacy_type_alias_definition()?;
if target_name.id == name_node.id {
return None;
}
let referenced_definition = self.legacy_type_alias_definition_for_name(
current_definition.file_scope(self.db()),
&name_node.id,
)?;
Some(TypeAliasType::Legacy(LegacyTypeAliasType::new(
self.db(),
&name_node.id,
referenced_definition,
)))
}
/// Returns the definition and target name for the legacy alias currently being inferred.
///
/// ```python
/// A = list["A"]
/// ```
fn current_legacy_type_alias_definition(
&self,
) -> Option<(Definition<'db>, &'ast ast::ExprName)> {
let InferenceRegion::Definition(definition) = self.region else {
return None;
};
let target = match definition.kind(self.db()) {
DefinitionKind::Assignment(assignment) => assignment.target(self.module()),
DefinitionKind::AnnotatedAssignment(assignment) => assignment.target(self.module()),
_ => return None,
};
Some((definition, target.as_name_expr()?))
}
/// Returns `true` if a definition is an assignment-based type alias we can preserve lazily.
///
/// ```python
/// A = list["A"]
///
/// from typing import TypeAlias
/// B: TypeAlias = list[B]
/// ```
fn is_legacy_type_alias_definition(&self, definition: Definition<'db>) -> bool {
if definition.file(self.db()) != self.file() {
return false;
}
match definition.kind(self.db()) {
DefinitionKind::Assignment(assignment) => {
assignment.target(self.module()).is_name_expr()
&& Self::is_legacy_type_alias_value_candidate(assignment.value(self.module()))
}
DefinitionKind::AnnotatedAssignment(assignment) => {
assignment.target(self.module()).is_name_expr()
&& assignment.value(self.module()).is_some()
&& Self::is_pep_613_type_alias_annotation(assignment.annotation(self.module()))
}
_ => false,
}
}
/// Returns `true` for annotations that mark a PEP 613 type alias declaration.
///
/// ```python
/// from typing import TypeAlias
///
/// A: TypeAlias = list[A]
/// ```
fn is_pep_613_type_alias_annotation(annotation: &ast::Expr) -> bool {
match annotation {
ast::Expr::Name(name) => name.id == "TypeAlias",
ast::Expr::Attribute(attribute) => attribute.attr.as_str() == "TypeAlias",
_ => false,
}
}
/// Finds the single reachable legacy alias definition for `name` in a file scope.
///
/// Ambiguous or non-alias bindings are rejected so recursive alias preservation does not turn a
/// runtime variable into a type-level alias:
///
/// ```python
/// A = list["B"]
/// B = list["A"]
/// ```
fn legacy_type_alias_definition_for_name(
&self,
file_scope_id: FileScopeId,
name: &str,
) -> Option<Definition<'db>> {
let place_table = self.index.place_table(file_scope_id);
let symbol_id = place_table.symbol_id(name)?;
let use_def = self.index.use_def_map(file_scope_id);
let bindings = use_def.reachable_symbol_bindings(symbol_id);
let reachability_constraints = bindings.reachability_constraints();
let predicates = bindings.predicates();
let mut result = None;
for binding in bindings {
let static_reachability = reachability_constraints.evaluate(
self.db(),
predicates,
binding.reachability_constraint,
);
if static_reachability.is_always_false() {
continue;
}
let DefinitionState::Defined(definition) = binding.binding else {
continue;
};
if !self.is_legacy_type_alias_definition(definition) {
return None;
}
if result.replace(definition).is_some() {
return None;
}
}
result
}
/// Returns `true` if an assignment value can be treated as a legacy type alias expression.
///
/// The accepted shape is intentionally syntactic and narrow:
///
/// ```python
/// A = list["A"]
/// B = int | str
/// C = None
/// ```
fn is_legacy_type_alias_value_candidate(value: &ast::Expr) -> bool {
match value {
ast::Expr::Name(_)
| ast::Expr::Attribute(_)
| ast::Expr::Subscript(_)
| ast::Expr::StringLiteral(_)
| ast::Expr::NoneLiteral(_) => true,
ast::Expr::BinOp(ast::ExprBinOp {
left,
op: ast::Operator::BitOr,
right,
..
}) => {
Self::is_legacy_type_alias_value_candidate(left)
&& Self::is_legacy_type_alias_value_candidate(right)
}
_ => false,
}
}
fn infer_local_place_load(
&self,
expr: PlaceExprRef,
@@ -24,7 +24,8 @@ pub(crate) fn check_pep_613_alias<'db>(
let mut speculative = builder.speculate();
speculative.typevar_binding_context = Some(definition);
speculative.context.inference_flags |= InferenceFlags::IN_TYPE_ALIAS;
speculative.context.inference_flags |=
InferenceFlags::IN_TYPE_ALIAS | InferenceFlags::IN_LEGACY_TYPE_ALIAS_VALUE;
speculative.infer_type_expression(value);
Some(speculative.context.finish())
}
@@ -214,9 +214,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
}
}
Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695(
_,
))) => {
Type::KnownInstance(KnownInstanceType::TypeAliasType(
TypeAliasType::ManualPEP695(_) | TypeAliasType::Legacy(_),
)) => {
let slice_ty = self.infer_expression(slice, TypeContext::default());
let mut variables = FxOrderSet::default();
slice_ty.bind_and_find_all_legacy_typevars(
@@ -133,8 +133,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
match expression {
ast::Expr::Name(name) => match name.ctx {
ast::ExprContext::Load => {
if let Some(type_alias) = self.self_referential_legacy_type_alias(name) {
return Type::TypeAlias(type_alias);
}
let ty = self.infer_name_expression(name);
self.infer_name_or_attribute_type_expression(ty, expression)
let ty = self.infer_name_or_attribute_type_expression(ty, expression);
if let Some(type_alias) = self.divergent_legacy_type_alias_reference(name, ty) {
return Type::TypeAlias(type_alias);
}
ty
}
ast::ExprContext::Invalid => Type::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
@@ -1541,8 +1549,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
}
}
KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695(_)) => {
// TODO: support generic "manual" PEP 695 type aliases
KnownInstanceType::TypeAliasType(
TypeAliasType::ManualPEP695(_) | TypeAliasType::Legacy(_),
) => {
// TODO: support generic non-PEP695 type aliases.
let slice_ty = self.infer_expression(slice, TypeContext::default());
let mut variables = FxOrderSet::default();
slice_ty.bind_and_find_all_legacy_typevars(
+137 -2
View File
@@ -1,3 +1,5 @@
use std::cell::{Cell, RefCell};
use itertools::Itertools;
use ruff_python_ast::name::Name;
use rustc_hash::FxHashSet;
@@ -14,6 +16,8 @@ use crate::types::enums::is_single_member_enum;
use crate::types::function::FunctionDecorators;
use crate::types::set_theoretic::RecursivelyDefined;
use crate::types::signatures::{ParametersKind, SignatureRelationVisitor};
use crate::types::type_alias::TypeAliasType;
use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard};
use crate::types::{
ApplyTypeMappingVisitor, CallableType, ClassBase, ClassLiteral, ClassType, CycleDetector,
IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, LiteralValueTypeKind,
@@ -23,8 +27,9 @@ use crate::types::{
use crate::{
Db,
types::{
ErrorContext, ErrorContextTree, Type, constraints::ConstraintSet,
generics::InferableTypeVars,
ErrorContext, ErrorContextTree, Type,
constraints::ConstraintSet,
generics::{InferableTypeVars, Specialization},
},
};
@@ -209,6 +214,92 @@ pub(crate) enum TypeRelation {
ConstraintSetAssignability,
}
struct SpecializingAliasCycleVisitor<'db> {
current_definition: Definition<'db>,
identity_specialization: Specialization<'db>,
active_specializations: RefCell<Vec<Specialization<'db>>>,
found: Cell<bool>,
recursion_guard: TypeCollector<'db>,
seen_aliases: RefCell<FxHashSet<Definition<'db>>>,
}
impl<'db> SpecializingAliasCycleVisitor<'db> {
fn visit_type_alias(&self, db: &'db dyn Db, alias: TypeAliasType<'db>) {
let definition = alias.definition(db);
if definition == self.current_definition {
if let Some(specialization) = alias.specialization(db) {
let normalized_specialization =
self.active_specializations.borrow().iter().rev().fold(
specialization,
|specialization, active_specialization| {
specialization.apply_specialization(db, *active_specialization)
},
);
if normalized_specialization != self.identity_specialization {
self.found.set(true);
}
}
return;
}
if !self.seen_aliases.borrow_mut().insert(definition) {
return;
}
let specialization_count = self.active_specializations.borrow().len();
if let Some(specialization) = alias.specialization(db) {
self.active_specializations
.borrow_mut()
.push(specialization);
}
self.visit_type(db, alias.raw_value_type(db));
self.active_specializations
.borrow_mut()
.truncate(specialization_count);
self.seen_aliases.borrow_mut().remove(&definition);
}
}
impl<'db> TypeVisitor<'db> for SpecializingAliasCycleVisitor<'db> {
fn should_visit_lazy_type_attributes(&self) -> bool {
false
}
fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {
if self.found.get() {
return;
}
if let Type::TypeAlias(alias) = ty {
self.visit_type_alias(db, alias);
return;
}
walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard);
}
}
fn has_specializing_alias_cycle<'db>(db: &'db dyn Db, alias: TypeAliasType<'db>) -> bool {
let Some(generic_context) = alias.generic_context(db) else {
return false;
};
let visitor = SpecializingAliasCycleVisitor {
current_definition: alias.definition(db),
identity_specialization: generic_context.identity_specialization(db),
active_specializations: RefCell::default(),
found: Cell::new(false),
recursion_guard: TypeCollector::default(),
seen_aliases: RefCell::default(),
};
visitor.visit_type(db, alias.raw_value_type(db));
visitor.found.get()
}
impl TypeRelation {
pub(crate) const fn is_assignability(self) -> bool {
matches!(self, TypeRelation::Assignability)
@@ -672,6 +763,14 @@ impl<'db, 'c> IsDisjointVisitor<'db, 'c> {
}
}
/// A fully specified invariant relation check used as an active-recursion key.
///
/// Materializing both sides of an invariant container can re-enter the same relation through a
/// recursive alias:
///
/// ```python
/// type RecursiveList = list[RecursiveList]
/// ```
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub(super) struct InvariantRelationGoal<'db> {
source_type: Type<'db>,
@@ -682,6 +781,14 @@ pub(super) struct InvariantRelationGoal<'db> {
}
impl<'db> InvariantRelationGoal<'db> {
/// Creates an active-recursion key for one invariant source/target relation.
///
/// The materialization kinds are part of the key because the same type pair can be checked in
/// distinct top and bottom positions for recursive aliases such as:
///
/// ```python
/// type RecursiveList = list[RecursiveList]
/// ```
pub(super) const fn new(
source_type: Type<'db>,
source_materialization: MaterializationKind,
@@ -1033,6 +1140,20 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> {
ConstraintSet::from_bool(self.constraints, self.relation.is_assignability())
}
(Type::TypeAlias(source_alias), _)
if has_specializing_alias_cycle(db, source_alias) =>
{
source.visit_type_alias_value(
db,
|| self.always(),
|source_alias_ty| {
self.with_recursion_guard(source, target, || {
self.check_type_pair(db, source_alias_ty, target)
})
},
)
}
(Type::TypeAlias(source_alias), _) => self.with_recursion_guard(source, target, || {
self.alias_relation_visitor.visit(
&AliasRelationGoal::Source {
@@ -1045,6 +1166,20 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> {
)
}),
(_, Type::TypeAlias(target_alias))
if has_specializing_alias_cycle(db, target_alias) =>
{
target.visit_type_alias_value(
db,
|| self.always(),
|target_alias_ty| {
self.with_recursion_guard(source, target, || {
self.check_type_pair(db, source, target_alias_ty)
})
},
)
}
(_, Type::TypeAlias(target_alias)) => self.with_recursion_guard(source, target, || {
self.alias_relation_visitor.visit(
&AliasRelationGoal::Target {
@@ -3,8 +3,8 @@ use std::fmt::Write;
use crate::{
Db,
types::{
ApplySpecialization, ApplyTypeMappingVisitor, GenericContext, Type, TypeContext,
TypeMapping,
ApplySpecialization, ApplyTypeMappingVisitor, GenericContext, InferenceFlags, Type,
TypeContext, TypeMapping,
cyclic::{TypeAliasRecursionVisitor, visit_type_alias},
definition_expression_type,
display::qualified_name_components_from_scope,
@@ -74,7 +74,7 @@ impl<'db> PEP695TypeAliasType<'db> {
},
heap_size=ruff_memory_usage::heap_size
)]
fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> {
pub(in crate::types) fn raw_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);
let type_alias_stmt_node = scope.node(db).expect_type_alias();
@@ -202,12 +202,99 @@ impl<'db> ManualPEP695TypeAliasType<'db> {
}
}
/// An assignment-based type alias (`Alias = ...` or `Alias: TypeAlias = ...`) that needs
/// lazy value-type computation to preserve recursive alias identity.
///
/// This is a type-level alias identity, not the runtime value of the assignment.
///
/// ```python
/// A = list["A"]
/// ```
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct LegacyTypeAliasType<'db> {
#[returns(ref)]
pub name: Name,
pub definition: Definition<'db>,
}
// The Salsa heap is tracked separately.
impl get_size2::GetSize for LegacyTypeAliasType<'_> {}
/// Visits the lazily computed value type of an assignment-based alias.
///
/// This keeps visitor traversal consistent with aliases whose value is not known until expansion,
/// including recursive aliases such as:
///
/// ```python
/// A = list["A"]
/// ```
pub(super) fn walk_legacy_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
db: &'db dyn Db,
type_alias: LegacyTypeAliasType<'db>,
visitor: &V,
) {
visitor.visit_type(db, type_alias.value_type(db));
}
#[salsa::tracked]
impl<'db> LegacyTypeAliasType<'db> {
/// The value type of this assignment-based type alias.
///
/// Computed lazily from the definition to avoid including the value in the interned
/// struct's identity. Returns `Divergent` if the type alias is defined cyclically.
///
/// ```python
/// from typing import TypeAlias
///
/// A: TypeAlias = list[A]
/// ```
#[salsa::tracked(
cycle_initial=|_, id, _| Type::divergent(id),
cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _| {
value.cycle_normalized(db, *previous, cycle)
},
heap_size=ruff_memory_usage::heap_size
)]
pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> {
let definition = self.definition(db);
let file = definition.file(db);
let module = parsed_module(db, file).load(db);
let value_node = match definition.kind(db) {
DefinitionKind::Assignment(assignment) => assignment.value(&module),
DefinitionKind::AnnotatedAssignment(assignment) => {
let Some(value) = assignment.value(&module) else {
return Type::unknown();
};
value
}
_ => return Type::unknown(),
};
definition_expression_type(db, definition, value_node)
.default_specialize(db)
.in_type_expression(
db,
definition.scope(db),
Some(definition),
InferenceFlags::empty(),
)
.unwrap_or_else(|_| Type::unknown())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, 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`.
ManualPEP695(ManualPEP695TypeAliasType<'db>),
/// An assignment-based type alias that needs to preserve its alias identity lazily.
///
/// ```python
/// A = list["A"]
/// ```
Legacy(LegacyTypeAliasType<'db>),
}
pub(super) fn walk_type_alias_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
@@ -225,6 +312,9 @@ pub(super) fn walk_type_alias_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
TypeAliasType::ManualPEP695(type_alias) => {
walk_manual_pep_695_type_alias(db, type_alias, visitor);
}
TypeAliasType::Legacy(type_alias) => {
walk_legacy_type_alias(db, type_alias, visitor);
}
}
}
@@ -233,6 +323,7 @@ impl<'db> TypeAliasType<'db> {
match self {
TypeAliasType::PEP695(type_alias) => type_alias.name(db),
TypeAliasType::ManualPEP695(type_alias) => type_alias.name(db),
TypeAliasType::Legacy(type_alias) => type_alias.name(db),
}
}
@@ -240,6 +331,7 @@ impl<'db> TypeAliasType<'db> {
match self {
TypeAliasType::PEP695(type_alias) => type_alias.definition(db),
TypeAliasType::ManualPEP695(type_alias) => type_alias.definition(db),
TypeAliasType::Legacy(type_alias) => type_alias.definition(db),
}
}
@@ -252,6 +344,7 @@ impl<'db> TypeAliasType<'db> {
match self {
TypeAliasType::PEP695(type_alias) => type_alias.value_type(db),
TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db),
TypeAliasType::Legacy(type_alias) => type_alias.value_type(db),
}
}
@@ -334,17 +427,18 @@ impl<'db> TypeAliasType<'db> {
/// Direct accessor for the alias value type before applying specialization.
///
/// Prefer [`TypeAliasType::visit_raw_value`] outside this module for recursive operations.
fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> {
pub(in crate::types) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> {
match self {
TypeAliasType::PEP695(type_alias) => type_alias.raw_value_type(db),
TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db),
TypeAliasType::Legacy(type_alias) => type_alias.value_type(db),
}
}
pub(crate) fn as_pep_695_type_alias(self) -> Option<PEP695TypeAliasType<'db>> {
match self {
TypeAliasType::PEP695(type_alias) => Some(type_alias),
TypeAliasType::ManualPEP695(_) => None,
TypeAliasType::ManualPEP695(_) | TypeAliasType::Legacy(_) => None,
}
}
@@ -352,21 +446,21 @@ impl<'db> TypeAliasType<'db> {
// TODO: Add support for generic non-PEP695 type aliases.
match self {
TypeAliasType::PEP695(type_alias) => type_alias.generic_context(db),
TypeAliasType::ManualPEP695(_) => None,
TypeAliasType::ManualPEP695(_) | TypeAliasType::Legacy(_) => None,
}
}
pub(crate) fn specialization(self, db: &'db dyn Db) -> Option<Specialization<'db>> {
match self {
TypeAliasType::PEP695(type_alias) => type_alias.specialization(db),
TypeAliasType::ManualPEP695(_) => None,
TypeAliasType::ManualPEP695(_) | TypeAliasType::Legacy(_) => None,
}
}
pub(super) fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> {
match self {
TypeAliasType::PEP695(type_alias) => type_alias.apply_function_specialization(db, ty),
TypeAliasType::ManualPEP695(_) => ty,
TypeAliasType::ManualPEP695(_) | TypeAliasType::Legacy(_) => ty,
}
}
@@ -379,7 +473,7 @@ impl<'db> TypeAliasType<'db> {
TypeAliasType::PEP695(type_alias) => {
TypeAliasType::PEP695(type_alias.apply_specialization(db, f))
}
TypeAliasType::ManualPEP695(_) => self,
TypeAliasType::ManualPEP695(_) | TypeAliasType::Legacy(_) => self,
}
}