mirror of
https://github.com/astral-sh/ruff.git
synced 2026-05-06 08:56:57 -04:00
[ty] Support recursive assignment-based type aliases
This commit is contained in:
@@ -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`"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user