diff --git a/crates/ty_python_semantic/resources/mdtest/call/constructor.md b/crates/ty_python_semantic/resources/mdtest/call/constructor.md index 02357c394b..8bede607da 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/ty_python_semantic/resources/mdtest/call/constructor.md @@ -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: diff --git a/crates/ty_python_semantic/resources/mdtest/call/dunder.md b/crates/ty_python_semantic/resources/mdtest/call/dunder.md index ebcb70ea06..632f0fab37 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/dunder.md +++ b/crates/ty_python_semantic/resources/mdtest/call/dunder.md @@ -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() diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index 15919bbdcb..ac00ab1c8b 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md index f88e39a08c..04e67ea37f 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 3331a31499..bc1cf0e084 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 51757bb9a2..c75b0fb70f 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -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"] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md index 1b94203ca2..f1b075e0e8 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -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: | -# 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[, Literal["str"]]`" if issubclass(t, (bytes, "str")): reveal_type(t) # revealed: | diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index 40321b8481..77ddfd55f4 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -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: +reveal_type(ClassInfo) # revealed: 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] diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 81fadedc7a..761fd32049 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 410a356e3f..0fceaa7d90 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -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`" diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index addfbf84ae..553a6c36b2 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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 { - match self { - Type::SpecialForm(special_form) => Some(special_form), - _ => None, - } - } - pub const fn as_property_instance(self) -> Option> { 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( 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( 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> { + /// 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> { 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() diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index e45d8b4d08..2cf6915f23 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -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 { + 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, + 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 { + 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; } diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index d3a4380cd8..1fbf24e5cb 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -52,9 +52,19 @@ pub(crate) type PairVisitor<'db, Tag, C> = CycleDetector, 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( db: &dyn Db, type_alias: TypeAliasType<'_>, @@ -269,6 +289,7 @@ struct ActiveRecursionGuard<'a, T: Hash + Eq> { } impl 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); } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 9a47349085..03aeb18cb7 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -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); + } } } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 6315390442..eacfa5133f 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -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; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 18ebea6684..02afdd68b5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -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> { + 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>, + active_specializations: &mut Vec>, + ) -> 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> { + 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> { + 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> { + 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, diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/pep_613_alias.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/pep_613_alias.rs index 5829c1654c..393feff003 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/pep_613_alias.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/pep_613_alias.rs @@ -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()) } diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index 965991ac2e..f54d919341 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -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( diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index b39d11bb22..494abe6e5c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -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( diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 0f64e6bf40..2dba1b2386 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -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>>, + found: Cell, + recursion_guard: TypeCollector<'db>, + seen_aliases: RefCell>>, +} + +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 { diff --git a/crates/ty_python_semantic/src/types/type_alias.rs b/crates/ty_python_semantic/src/types/type_alias.rs index 4a1237c5f8..c922be6516 100644 --- a/crates/ty_python_semantic/src/types/type_alias.rs +++ b/crates/ty_python_semantic/src/types/type_alias.rs @@ -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> { 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> { 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, } }