From 546c7e43c082f91a3e586d4f3d2b9e4377496c86 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 22 Aug 2025 17:15:32 -0700 Subject: [PATCH 1/2] [ty] support PEP 613 typing.TypeAlias --- .../annotations/unsupported_special_forms.md | 2 +- .../resources/mdtest/attributes.md | 4 +- .../resources/mdtest/binary/instances.md | 3 +- .../resources/mdtest/binary/integers.md | 6 +- .../resources/mdtest/expression/lambda.md | 2 +- .../resources/mdtest/narrow/isinstance.md | 6 +- .../resources/mdtest/narrow/issubclass.md | 11 +- .../resources/mdtest/pep613_type_aliases.md | 272 ++++++++++++++++++ .../resources/mdtest/pep695_type_aliases.md | 2 +- .../resources/mdtest/subscript/tuple.md | 4 +- .../resources/mdtest/sys_version_info.md | 2 +- .../src/semantic_index/definition.rs | 9 + crates/ty_python_semantic/src/types.rs | 167 ++++++++--- crates/ty_python_semantic/src/types/class.rs | 4 +- .../src/types/class_base.rs | 6 +- .../src/types/ide_support.rs | 8 +- .../src/types/infer/builder.rs | 70 ++++- .../types/infer/builder/type_expression.rs | 7 + .../src/types/type_ordering.rs | 3 - 19 files changed, 493 insertions(+), 95 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 8a6f499655a7f..cf39a4e5db507 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -16,7 +16,7 @@ Alias: TypeAlias = int def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...] - reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`) + reveal_type(Alias) # revealed: Alias return args def g() -> TypeGuard[int]: ... diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 496f796c7a94f..50fe36dce452e 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2187,9 +2187,9 @@ reveal_type(False.real) # revealed: Literal[0] All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`: ```py -# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes +# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[Buffer], /) -> bytes reveal_type(b"foo".join) -# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool +# revealed: bound method Literal[b"foo"].endswith(suffix: Buffer | tuple[Buffer, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool reveal_type(b"foo".endswith) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/instances.md b/crates/ty_python_semantic/resources/mdtest/binary/instances.md index c27e70ed76e0f..dab5751d666e4 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/instances.md @@ -313,8 +313,7 @@ reveal_type(A() + "foo") # revealed: A reveal_type("foo" + A()) # revealed: A reveal_type(A() + b"foo") # revealed: A -# TODO should be `A` since `bytes.__add__` doesn't support `A` instances -reveal_type(b"foo" + A()) # revealed: bytes +reveal_type(b"foo" + A()) # revealed: A reveal_type(A() + ()) # revealed: A reveal_type(() + A()) # revealed: A diff --git a/crates/ty_python_semantic/resources/mdtest/binary/integers.md b/crates/ty_python_semantic/resources/mdtest/binary/integers.md index 95561a295ee50..a021a15ae14a9 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/integers.md @@ -54,10 +54,8 @@ reveal_type(2**largest_u32) # revealed: int def variable(x: int): reveal_type(x**2) # revealed: int - # TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching - reveal_type(2**x) # revealed: int - # TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching - reveal_type(x**x) # revealed: int + reveal_type(2**x) # revealed: Any + reveal_type(x**x) # revealed: Any ``` If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but diff --git a/crates/ty_python_semantic/resources/mdtest/expression/lambda.md b/crates/ty_python_semantic/resources/mdtest/expression/lambda.md index b48efaad70bf4..a705cc708953d 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/lambda.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/lambda.md @@ -127,7 +127,7 @@ x = lambda y: y reveal_type(x.__code__) # revealed: CodeType reveal_type(x.__name__) # revealed: str reveal_type(x.__defaults__) # revealed: tuple[Any, ...] | None -reveal_type(x.__annotations__) # revealed: dict[str, @Todo(Support for `typing.TypeAlias`)] +reveal_type(x.__annotations__) # revealed: dict[str, AnnotationForm] reveal_type(x.__dict__) # revealed: dict[str, Any] reveal_type(x.__doc__) # revealed: str | None reveal_type(x.__kwdefaults__) # revealed: dict[str, Any] | None diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 60ec2fa84471e..ee3719768a750 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -146,13 +146,11 @@ def _(flag: bool): def _(flag: bool): x = 1 if flag else "a" - # TODO: this should cause us to emit a diagnostic during - # type checking + # error: [invalid-argument-type] if isinstance(x, "a"): reveal_type(x) # revealed: Literal[1, "a"] - # TODO: this should cause us to emit a diagnostic during - # type checking + # error: [invalid-argument-type] 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 ce77126d32156..aa057d144bd90 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -214,20 +214,13 @@ def flag() -> bool: t = int if flag() else str -# TODO: this should cause us to emit a diagnostic during -# type checking +# error: [invalid-argument-type] if issubclass(t, "str"): reveal_type(t) # revealed: | -# TODO: this should cause us to emit a diagnostic during -# type checking +# TODO error: [invalid-argument-type] if issubclass(t, (bytes, "str")): reveal_type(t) # revealed: | - -# TODO: this should cause us to emit a diagnostic during -# type checking -if issubclass(t, Any): - reveal_type(t) # revealed: | ``` ### Do not narrow if there are keyword arguments diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md new file mode 100644 index 0000000000000..0648b9ca16ca2 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -0,0 +1,272 @@ +# PEP 613 explicit type aliases + +```toml +[environment] +python-version = "3.10" +``` + +Explicit type aliases were introduced in PEP 613. They are defined using an annotated-assignment +statement, annotated with `typing.TypeAlias`: + +## Basic + +```py +from typing import TypeAlias + +MyInt: TypeAlias = int + +def f(x: MyInt): + reveal_type(x) # revealed: int + +f(1) +``` + +## Union + +For more complex type aliases, such as those involving unions or generics, the inferred value type +of the right-hand side is not a valid type for use in a type expression, and we need to infer it as +a type expression. + +### Old syntax + +```py +from typing import TypeAlias, Union + +IntOrStr: TypeAlias = Union[int, str] + +def f(x: IntOrStr): + reveal_type(x) # revealed: int | str + if isinstance(x, int): + reveal_type(x) # revealed: int + else: + reveal_type(x) # revealed: str + +f(1) +f("foo") +``` + +### New syntax + +```py +from typing import TypeAlias + +IntOrStr: TypeAlias = int | str + +def f(x: IntOrStr): + reveal_type(x) # revealed: int | str + if isinstance(x, int): + reveal_type(x) # revealed: int + else: + reveal_type(x) # revealed: str + +f(1) +f("foo") +``` + +### Name resolution is not deferred + +Unlike with a PEP 695 type alias, the right-hand side of a PEP 613 type alias is evaluated +immediately, name resolution is not deferred. + +```py +from typing import TypeAlias + +A: TypeAlias = B | None # error: [unresolved-reference] +B: TypeAlias = int + +def _(a: A): + reveal_type(a) # revealed: Unknown | None +``` + +## Multiple layers of union aliases + +```py +from typing import TypeAlias + +class A: ... +class B: ... +class C: ... +class D: ... + +W: TypeAlias = A | B +X: TypeAlias = C | D +Y: TypeAlias = W | X + +from ty_extensions import is_equivalent_to, static_assert + +static_assert(is_equivalent_to(Y, A | B | C | D)) +``` + +## Cycles + +We also support cyclic type aliases: + +### Old syntax + +```py +from typing import Union, TypeAlias + +MiniJSON: TypeAlias = Union[int, str, list["MiniJSON"]] + +def f(x: MiniJSON): + reveal_type(x) # revealed: int | str | list[MiniJSON] + if isinstance(x, int): + reveal_type(x) # revealed: int + elif isinstance(x, str): + reveal_type(x) # revealed: str + else: + reveal_type(x) # revealed: list[MiniJSON] + +f(1) +f("foo") +f([1, "foo"]) +``` + +### New syntax + +```py +from typing import TypeAlias + +MiniJSON: TypeAlias = int | str | list["MiniJSON"] + +def f(x: MiniJSON): + reveal_type(x) # revealed: int | str | list[MiniJSON] + if isinstance(x, int): + reveal_type(x) # revealed: int + elif isinstance(x, str): + reveal_type(x) # revealed: str + else: + reveal_type(x) # revealed: list[MiniJSON] + +f(1) +f("foo") +f([1, "foo"]) +``` + +### Generic + +```py +from typing import TypeAlias, Generic, TypeVar, Union + +T = TypeVar("T") + +Alias: TypeAlias = Union[list["Alias"], int] + +class A(Generic[T]): + pass + +class B(A[Alias]): + pass +``` + +### Mutually recursive + +```py +from typing import TypeAlias + +A: TypeAlias = tuple["B"] | None +B: TypeAlias = tuple[A] | None + +def f(x: A): + if x is not None: + reveal_type(x) # revealed: tuple[B] + y = x[0] + if y is not None: + reveal_type(y) # revealed: tuple[A] + +def g(x: A | B): + reveal_type(x) # revealed: tuple[B] | None + +from ty_extensions import Intersection + +def h(x: Intersection[A, B]): + reveal_type(x) # revealed: tuple[B] | None +``` + +### Self-recursive callable type + +```py +from typing import Callable, TypeAlias + +C: TypeAlias = Callable[[], "C" | None] + +def _(x: C): + reveal_type(x) # revealed: () -> C | None +``` + +### Union inside generic + +#### With old-style union + +```py +from typing import Union, TypeAlias + +A: TypeAlias = list[Union["A", str]] + +def f(x: A): + reveal_type(x) # revealed: list[A | str] + for item in x: + reveal_type(item) # revealed: list[A | str] | str +``` + +#### With new-style union + +```py +from typing import TypeAlias + +A: TypeAlias = list["A" | str] + +def f(x: A): + reveal_type(x) # revealed: list[A | str] + for item in x: + reveal_type(item) # revealed: list[A | str] | str +``` + +#### With Optional + +```py +from typing import Optional, Union, TypeAlias + +A: TypeAlias = list[Optional[Union["A", str]]] + +def f(x: A): + reveal_type(x) # revealed: list[A | str | None] + for item in x: + reveal_type(item) # revealed: list[A | str | None] | str | None +``` + +### Invalid examples + +#### No value + +```py +from typing import TypeAlias + +# TODO: error +Bad: TypeAlias + +# Nested function so we don't emit unresolved-reference for `Bad`: +def _(): + def f(x: Bad): + reveal_type(x) # revealed: Unknown +``` + +#### No value, in stub + +`stub.pyi`: + +```pyi +from typing import TypeAlias + +# TODO: error +Bad: TypeAlias +``` + +`main.py`: + +```py +from stub import Bad + +def f(x: Bad): + reveal_type(x) # revealed: Unknown +``` 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 a0f1c3fd8b2f4..ae2b66f073044 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -28,7 +28,7 @@ def f() -> None: ```py type IntOrStr = int | str -reveal_type(IntOrStr.__value__) # revealed: @Todo(Support for `typing.TypeAlias`) +reveal_type(IntOrStr.__value__) # revealed: Any ``` ## Invalid assignment diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md index 97dfe6439e6a2..f1960171dae08 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md @@ -147,8 +147,8 @@ But perhaps the most commonly used tuple subclass instance is the singleton `sys ```py import sys -# revealed: Overload[(self, index: Literal[-5, 0], /) -> Literal[3], (self, index: Literal[-4, 1], /) -> Literal[11], (self, index: Literal[-3, -1, 2, 4], /) -> int, (self, index: Literal[-2, 3], /) -> Literal["alpha", "beta", "candidate", "final"], (self, index: SupportsIndex, /) -> int | Literal["alpha", "beta", "candidate", "final"], (self, index: slice[Any, Any, Any], /) -> tuple[int | Literal["alpha", "beta", "candidate", "final"], ...]] -reveal_type(type(sys.version_info).__getitem__) +# TODO revealed: Overload[(self, index: Literal[-5, 0], /) -> Literal[3], (self, index: Literal[-4, 1], /) -> Literal[11], (self, index: Literal[-3, -1, 2, 4], /) -> int, (self, index: Literal[-2, 3], /) -> Literal["alpha", "beta", "candidate", "final"], (self, index: SupportsIndex, /) -> int | Literal["alpha", "beta", "candidate", "final"], (self, index: slice[Any, Any, Any], /) -> tuple[int | Literal["alpha", "beta", "candidate", "final"], ...]] +reveal_type(type(sys.version_info).__getitem__) # revealed: Unknown ``` Because of the synthesized `__getitem__` overloads we synthesize for tuples and tuple subclasses, diff --git a/crates/ty_python_semantic/resources/mdtest/sys_version_info.md b/crates/ty_python_semantic/resources/mdtest/sys_version_info.md index bedcdb20b9e05..66af7d5297906 100644 --- a/crates/ty_python_semantic/resources/mdtest/sys_version_info.md +++ b/crates/ty_python_semantic/resources/mdtest/sys_version_info.md @@ -122,7 +122,7 @@ properties on instance types: ```py reveal_type(sys.version_info.micro) # revealed: int -reveal_type(sys.version_info.releaselevel) # revealed: @Todo(Support for `typing.TypeAlias`) +reveal_type(sys.version_info.releaselevel) # revealed: Literal["alpha", "beta", "candidate", "final"] reveal_type(sys.version_info.serial) # revealed: int ``` diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 368994fd3436a..536b1e0437c21 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -693,6 +693,15 @@ impl DefinitionKind<'_> { } } + pub(crate) const fn as_annotated_assignment( + &self, + ) -> Option<&AnnotatedAssignmentDefinitionKind> { + match self { + DefinitionKind::AnnotatedAssignment(annotated_assignment) => Some(annotated_assignment), + _ => None, + } + } + pub(crate) fn is_import(&self) -> bool { matches!( self, diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 97e86737f8c18..bbbc7ae5e21be 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6257,7 +6257,9 @@ impl<'db> Type<'db> { }), Type::KnownInstance(known_instance) => match known_instance { - KnownInstanceType::TypeAliasType(alias) => Ok(Type::TypeAlias(*alias)), + KnownInstanceType::TypeAliasType(alias) | KnownInstanceType::TypeAlias(alias) => { + Ok(Type::TypeAlias(*alias)) + } KnownInstanceType::TypeVar(typevar) => { let index = semantic_index(db, scope_id.file(db)); Ok(bind_typevar( @@ -6353,7 +6355,7 @@ impl<'db> Type<'db> { ) .unwrap_or(*self)) } - SpecialFormType::TypeAlias => Ok(Type::Dynamic(DynamicType::TodoTypeAlias)), + SpecialFormType::TypeAlias => Ok(*self), SpecialFormType::TypedDict => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec_inline![ InvalidTypeExpression::TypedDict @@ -7490,6 +7492,10 @@ pub enum KnownInstanceType<'db> { /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) TypeAliasType(TypeAliasType<'db>), + /// A single instance of a PEP 613 type alias (in other words, an arbitrary type form at + /// runtime.) + TypeAlias(TypeAliasType<'db>), + /// A single instance of `warnings.deprecated` or `typing_extensions.deprecated` Deprecated(DeprecatedInstance<'db>), @@ -7514,7 +7520,7 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( KnownInstanceType::TypeVar(typevar) => { visitor.visit_type_var_type(db, typevar); } - KnownInstanceType::TypeAliasType(type_alias) => { + KnownInstanceType::TypeAliasType(type_alias) | KnownInstanceType::TypeAlias(type_alias) => { visitor.visit_type_alias_type(db, type_alias); } KnownInstanceType::Deprecated(_) | KnownInstanceType::ConstraintSet(_) => { @@ -7538,7 +7544,7 @@ impl<'db> KnownInstanceType<'db> { Self::SubscriptedGeneric(context.normalized_impl(db, visitor)) } Self::TypeVar(typevar) => Self::TypeVar(typevar.normalized_impl(db, visitor)), - Self::TypeAliasType(type_alias) => { + Self::TypeAliasType(type_alias) | Self::TypeAlias(type_alias) => { Self::TypeAliasType(type_alias.normalized_impl(db, visitor)) } Self::Deprecated(deprecated) => { @@ -7561,6 +7567,8 @@ impl<'db> KnownInstanceType<'db> { KnownClass::GenericAlias } Self::TypeAliasType(_) => KnownClass::TypeAliasType, + // A PEP 613 type alias could be any type form object at runtime: + Self::TypeAlias(_) => KnownClass::Object, Self::Deprecated(_) => KnownClass::Deprecated, Self::Field(_) => KnownClass::Field, Self::ConstraintSet(_) => KnownClass::ConstraintSet, @@ -7617,6 +7625,7 @@ impl<'db> KnownInstanceType<'db> { f.write_str("typing.TypeAliasType") } } + KnownInstanceType::TypeAlias(alias) => f.write_str(alias.name(self.db)), // This is a legacy `TypeVar` _outside_ of any generic class or function, so we render // it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll // have a `Type::TypeVar(_)`, which is rendered as the typevar's name. @@ -7684,9 +7693,6 @@ pub enum DynamicType<'db> { /// A special Todo-variant for PEP-695 `ParamSpec` types. A temporary variant to detect and special- /// case the handling of these types in `Callable` annotations. TodoPEP695ParamSpec, - /// A special Todo-variant for type aliases declared using `typing.TypeAlias`. - /// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions. - TodoTypeAlias, /// A special Todo-variant for `Unpack[Ts]`, so that we can treat it specially in `Generic[Unpack[Ts]]` TodoUnpack, /// A type that is determined to be divergent during type inference for a recursive function. @@ -7725,13 +7731,6 @@ impl std::fmt::Display for DynamicType<'_> { f.write_str("@Todo") } } - DynamicType::TodoTypeAlias => { - if cfg!(debug_assertions) { - f.write_str("@Todo(Support for `typing.TypeAlias`)") - } else { - f.write_str("@Todo") - } - } DynamicType::Divergent(_) => f.write_str("Divergent"), } } @@ -10779,12 +10778,14 @@ impl<'db> ModuleLiteralType<'db> { } } +/// A PEP 695 type alias, created by the `type` statement. +/// /// # Ordering /// Ordering is based on the type alias's salsa-assigned id and not on its values. /// The id may change between runs, or when the alias was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] #[derive(PartialOrd, Ord)] -pub struct PEP695TypeAliasType<'db> { +pub struct PEP695TypeAlias<'db> { #[returns(ref)] pub name: ast::name::Name, @@ -10794,25 +10795,25 @@ pub struct PEP695TypeAliasType<'db> { } // The Salsa heap is tracked separately. -impl get_size2::GetSize for PEP695TypeAliasType<'_> {} +impl get_size2::GetSize for PEP695TypeAlias<'_> {} fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, - type_alias: PEP695TypeAliasType<'db>, + type_alias: PEP695TypeAlias<'db>, visitor: &V, ) { visitor.visit_type(db, type_alias.value_type(db)); } #[salsa::tracked] -impl<'db> PEP695TypeAliasType<'db> { +impl<'db> PEP695TypeAlias<'db> { pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { let scope = self.rhs_scope(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) } - #[salsa::tracked(cycle_fn=value_type_cycle_recover, cycle_initial=value_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_fn=pep695_alias_value_type_cycle_recover, cycle_initial=pep695_alias_value_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { let scope = self.rhs_scope(db); let module = parsed_module(db, scope.file(db)).load(db); @@ -10836,7 +10837,7 @@ impl<'db> PEP695TypeAliasType<'db> { self, db: &'db dyn Db, f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, - ) -> PEP695TypeAliasType<'db> { + ) -> PEP695TypeAlias<'db> { match self.generic_context(db) { None => self, @@ -10847,12 +10848,7 @@ impl<'db> PEP695TypeAliasType<'db> { // `typing.TypeAliasType` internally, and pass the specialization through to the value type, // except when resolving to an instance of the type alias, or its display representation. let specialization = f(generic_context); - PEP695TypeAliasType::new( - db, - self.name(db), - self.rhs_scope(db), - Some(specialization), - ) + PEP695TypeAlias::new(db, self.name(db), self.rhs_scope(db), Some(specialization)) } } } @@ -10889,28 +10885,31 @@ fn generic_context_cycle_recover<'db>( _db: &'db dyn Db, _value: &Option>, _count: u32, - _self: PEP695TypeAliasType<'db>, + _self: PEP695TypeAlias<'db>, ) -> salsa::CycleRecoveryAction>> { salsa::CycleRecoveryAction::Iterate } fn generic_context_cycle_initial<'db>( _db: &'db dyn Db, - _self: PEP695TypeAliasType<'db>, + _self: PEP695TypeAlias<'db>, ) -> Option> { None } -fn value_type_cycle_recover<'db>( +fn pep695_alias_value_type_cycle_recover<'db>( _db: &'db dyn Db, _value: &Type<'db>, _count: u32, - _self: PEP695TypeAliasType<'db>, + _self: PEP695TypeAlias<'db>, ) -> salsa::CycleRecoveryAction> { salsa::CycleRecoveryAction::Iterate } -fn value_type_cycle_initial<'db>(_db: &'db dyn Db, _self: PEP695TypeAliasType<'db>) -> Type<'db> { +fn pep695_alias_value_type_cycle_initial<'db>( + _db: &'db dyn Db, + _self: PEP695TypeAlias<'db>, +) -> Type<'db> { Type::Never } @@ -10921,7 +10920,7 @@ fn value_type_cycle_initial<'db>(_db: &'db dyn Db, _self: PEP695TypeAliasType<'d /// The id may change between runs, or when the alias was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] #[derive(PartialOrd, Ord)] -pub struct ManualPEP695TypeAliasType<'db> { +pub struct ManualPEP695TypeAlias<'db> { #[returns(ref)] pub name: ast::name::Name, pub definition: Option>, @@ -10929,17 +10928,17 @@ pub struct ManualPEP695TypeAliasType<'db> { } // The Salsa heap is tracked separately. -impl get_size2::GetSize for ManualPEP695TypeAliasType<'_> {} +impl get_size2::GetSize for ManualPEP695TypeAlias<'_> {} fn walk_manual_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, - type_alias: ManualPEP695TypeAliasType<'db>, + type_alias: ManualPEP695TypeAlias<'db>, visitor: &V, ) { visitor.visit_type(db, type_alias.value(db)); } -impl<'db> ManualPEP695TypeAliasType<'db> { +impl<'db> ManualPEP695TypeAlias<'db> { fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { Self::new( db, @@ -10950,14 +10949,89 @@ impl<'db> ManualPEP695TypeAliasType<'db> { } } +/// A PEP 613 type alias, annotated with `: typing.TypeAlias`. +/// +/// # Ordering +/// Ordering is based on the type alias's salsa-assigned id and not on its values. +/// The id may change between runs, or when the alias was garbage collected and recreated. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] +pub struct PEP613TypeAlias<'db> { + #[returns(ref)] + pub name: ast::name::Name, + + /// The definition that created this type alias. + pub definition: Definition<'db>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for PEP613TypeAlias<'_> {} + +fn walk_pep_613_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + type_alias: PEP613TypeAlias<'db>, + visitor: &V, +) { + visitor.visit_type(db, type_alias.value_type(db)); +} + +#[salsa::tracked] +impl<'db> PEP613TypeAlias<'db> { + /// The type that this type alias refers to. + #[salsa::tracked(cycle_fn=pep613_alias_value_type_cycle_recover, cycle_initial=pep613_alias_value_type_cycle_initial, 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 module = parsed_module(db, definition.file(db)).load(db); + let value_node = self + .definition(db) + .kind(db) + .as_annotated_assignment() + // SAFETY: type inference won't create a PEP 613 type alias for any definition other + // than an annotated assignment. + .unwrap() + .value(&module) + // SAFETY: type inference won't create a PEP 613 type alias for an annotated assignment + // with no right-hand side. + .unwrap(); + definition_expression_type(db, definition, value_node) + } + + fn normalized_impl(self, _db: &'db dyn Db, _visitor: &NormalizedVisitor<'db>) -> Self { + self + } +} + +fn pep613_alias_value_type_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Type<'db>, + _count: u32, + _self: PEP613TypeAlias<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn pep613_alias_value_type_cycle_initial<'db>( + _db: &'db dyn Db, + _self: PEP613TypeAlias<'db>, +) -> Type<'db> { + Type::Never +} + +/// Represents a type alias, whether PEP 695 or PEP 613. +/// +/// PEP 613 type aliases are annotated with `: typing.TypeAlias`. PEP 695 type aliases are created +/// via instances of `types.TypeAliasType`, whether manually instantiated or via the `type` +/// statement. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update, get_size2::GetSize, )] pub enum TypeAliasType<'db> { /// A type alias defined using the PEP 695 `type` statement. - PEP695(PEP695TypeAliasType<'db>), + PEP695(PEP695TypeAlias<'db>), /// A type alias defined by manually instantiating the PEP 695 `types.TypeAliasType`. - ManualPEP695(ManualPEP695TypeAliasType<'db>), + ManualPEP695(ManualPEP695TypeAlias<'db>), + /// A type alias defined with an annotation of the PEP 613 `typing.TypeAlias` type. + PEP613(PEP613TypeAlias<'db>), } fn walk_type_alias_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -10975,6 +11049,9 @@ 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::PEP613(type_alias) => { + walk_pep_613_type_alias(db, type_alias, visitor); + } } } @@ -10987,6 +11064,9 @@ impl<'db> TypeAliasType<'db> { TypeAliasType::ManualPEP695(type_alias) => { TypeAliasType::ManualPEP695(type_alias.normalized_impl(db, visitor)) } + TypeAliasType::PEP613(type_alias) => { + TypeAliasType::PEP613(type_alias.normalized_impl(db, visitor)) + } } } @@ -10994,6 +11074,7 @@ impl<'db> TypeAliasType<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.name(db), TypeAliasType::ManualPEP695(type_alias) => type_alias.name(db), + TypeAliasType::PEP613(type_alias) => type_alias.name(db), } } @@ -11001,6 +11082,7 @@ impl<'db> TypeAliasType<'db> { match self { TypeAliasType::PEP695(type_alias) => Some(type_alias.definition(db)), TypeAliasType::ManualPEP695(type_alias) => type_alias.definition(db), + TypeAliasType::PEP613(type_alias) => Some(type_alias.definition(db)), } } @@ -11008,13 +11090,14 @@ impl<'db> TypeAliasType<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.value_type(db), TypeAliasType::ManualPEP695(type_alias) => type_alias.value(db), + TypeAliasType::PEP613(type_alias) => type_alias.value_type(db), } } - pub(crate) fn as_pep_695_type_alias(self) -> Option> { + pub(crate) fn as_pep_695_type_alias(self) -> Option> { match self { TypeAliasType::PEP695(type_alias) => Some(type_alias), - TypeAliasType::ManualPEP695(_) => None, + _ => None, } } @@ -11022,14 +11105,14 @@ 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::PEP613(_) => 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::PEP613(_) => None, } } @@ -11042,7 +11125,7 @@ impl<'db> TypeAliasType<'db> { TypeAliasType::PEP695(type_alias) => { TypeAliasType::PEP695(type_alias.apply_specialization(db, f)) } - TypeAliasType::ManualPEP695(_) => self, + TypeAliasType::ManualPEP695(_) | TypeAliasType::PEP613(_) => self, } } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index da30aa3144f72..c4a41d6717939 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -34,7 +34,7 @@ use crate::types::visitor::{NonAtomicType, TypeKind, TypeVisitor, walk_non_atomi use crate::types::{ ApplyTypeMappingVisitor, Binding, BoundSuperType, CallableType, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, - IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, + IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAlias, MaterializationKind, NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeContext, TypeMapping, TypeRelation, TypedDictParams, UnionBuilder, VarianceInferable, declaration_type, determine_upper_bound, infer_definition_types, @@ -5328,7 +5328,7 @@ impl KnownClass { return; }; overload.set_return_type(Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( + TypeAliasType::ManualPEP695(ManualPEP695TypeAlias::new( db, ast::name::Name::new(name.value(db)), containing_assignment, diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 204cbfb350892..eb8cd3da9e61d 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -49,10 +49,7 @@ impl<'db> ClassBase<'db> { ClassBase::Dynamic(DynamicType::Any) => "Any", ClassBase::Dynamic(DynamicType::Unknown) => "Unknown", ClassBase::Dynamic( - DynamicType::Todo(_) - | DynamicType::TodoPEP695ParamSpec - | DynamicType::TodoTypeAlias - | DynamicType::TodoUnpack, + DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec | DynamicType::TodoUnpack, ) => "@Todo", ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent", ClassBase::Protocol => "Protocol", @@ -168,6 +165,7 @@ impl<'db> ClassBase<'db> { KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic), KnownInstanceType::SubscriptedProtocol(_) => Some(Self::Protocol), KnownInstanceType::TypeAliasType(_) + | KnownInstanceType::TypeAlias(_) | KnownInstanceType::TypeVar(_) | KnownInstanceType::Deprecated(_) | KnownInstanceType::Field(_) diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 0dfa18dbed0f2..db8a3e62d3ca3 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -13,8 +13,7 @@ use crate::semantic_index::{ use crate::types::call::{CallArguments, MatchedArgument}; use crate::types::signatures::Signature; use crate::types::{ - ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type, - class::CodeGeneratorKind, + ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, class::CodeGeneratorKind, }; use crate::{Db, HasType, NameKind, SemanticModel}; use ruff_db::files::{File, FileRange}; @@ -268,9 +267,10 @@ impl<'db> AllMembers<'db> { } Type::ClassLiteral(class) if class.is_protocol(db) => continue, Type::KnownInstance( - KnownInstanceType::TypeVar(_) | KnownInstanceType::TypeAliasType(_), + KnownInstanceType::TypeVar(_) + | KnownInstanceType::TypeAliasType(_) + | KnownInstanceType::TypeAlias(_), ) => continue, - Type::Dynamic(DynamicType::TodoTypeAlias) => continue, _ => {} } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 7155e1f7a1be4..7cc9633c71141 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -93,9 +93,9 @@ use crate::types::visitor::any_over_type; use crate::types::{ CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, - MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm, - Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type, - TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, + MemberLookupPolicy, MetaclassCandidate, PEP613TypeAlias, PEP695TypeAlias, Parameter, + ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, + Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, binding_type, todo_type, @@ -1281,6 +1281,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { DefinitionKind::Assignment(assignment) => { self.infer_assignment_deferred(assignment.value(self.module())); } + DefinitionKind::AnnotatedAssignment(annotated_assignment) => { + self.infer_annotated_assignment_deferred(annotated_assignment); + } _ => {} } } @@ -2709,7 +2712,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .to_scope_id(self.db(), self.file()); let type_alias_ty = Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::PEP695(PEP695TypeAliasType::new( + TypeAliasType::PEP695(PEP695TypeAlias::new( self.db(), &type_alias.name.as_name_expr().unwrap().id, rhs_scope, @@ -4474,6 +4477,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + // Handle assignments to `TYPE_CHECKING`. if target .as_name_expr() .is_some_and(|name| &name.id == "TYPE_CHECKING") @@ -4502,6 +4506,43 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { declared.inner = Type::BooleanLiteral(true); } + // If the target of an assignment is not one of the place expressions we support, + // then they are not definitions, so we can only be here if the target is in a form supported as a place expression. + // In this case, we can simply store types in `target` below, instead of calling `infer_expression` (which would return `Never`). + debug_assert!(PlaceExpr::try_from_expr(target).is_some()); + + // Handle PEP 613 type aliases. + if matches!( + declared.inner, + Type::SpecialForm(SpecialFormType::TypeAlias) + ) { + let ty = if let Some(name_expr) = target.as_name_expr() { + if value.is_some() { + self.deferred.insert(definition); + Type::KnownInstance(KnownInstanceType::TypeAlias(TypeAliasType::PEP613( + PEP613TypeAlias::new(self.db(), name_expr.id.clone(), definition), + ))) + } else { + // TODO diagnostic: no RHS + Type::unknown() + } + } else { + // TODO diagnostic: target is not a name + Type::unknown() + }; + // TODO diagnostic if qualifiers are not empty + if value.is_some() || self.in_stub() { + self.add_declaration_with_binding( + target.into(), + definition, + &DeclaredAndInferredType::AreTheSame(TypeAndQualifiers::declared(ty)), + ); + } else { + self.add_declaration(target.into(), definition, TypeAndQualifiers::declared(ty)); + } + return; + } + // Handle various singletons. if let Some(name_expr) = target.as_name_expr() { if let Some(special_form) = @@ -4511,11 +4552,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // If the target of an assignment is not one of the place expressions we support, - // then they are not definitions, so we can only be here if the target is in a form supported as a place expression. - // In this case, we can simply store types in `target` below, instead of calling `infer_expression` (which would return `Never`). - debug_assert!(PlaceExpr::try_from_expr(target).is_some()); - if let Some(value) = value { let inferred_ty = self.infer_maybe_standalone_expression( value, @@ -4557,6 +4593,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + fn infer_annotated_assignment_deferred( + &mut self, + assignment: &'db AnnotatedAssignmentDefinitionKind, + ) { + // Annotated assignments defer the entire value expression in case of a PEP 613 type alias. + // SAFETY `infer_annotated_assignment_definition` does not defer it there is no RHS + let value_node = assignment.value(self.module()).unwrap(); + self.infer_type_expression(value_node); + } + fn infer_augmented_assignment_statement(&mut self, assignment: &ast::StmtAugAssign) { if assignment.target.is_name_expr() { self.infer_definition(assignment); @@ -7783,8 +7829,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { todo @ Type::Dynamic( DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec - | DynamicType::TodoUnpack - | DynamicType::TodoTypeAlias, + | DynamicType::TodoUnpack, ), _, _, @@ -7794,8 +7839,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { todo @ Type::Dynamic( DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec - | DynamicType::TodoUnpack - | DynamicType::TodoTypeAlias, + | DynamicType::TodoUnpack, ), _, ) => Some(todo), 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 69c6b8a165d15..71433004f3574 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 @@ -811,6 +811,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_type_expression(slice); todo_type!("Generic manual PEP-695 type alias") } + KnownInstanceType::TypeAliasType(TypeAliasType::PEP613(_)) => { + unreachable!("PEP 613 type aliases are not KnownInstance::TypeAliasType"); + } + KnownInstanceType::TypeAlias(_) => { + self.infer_type_expression(&subscript.slice); + todo_type!("Generic PEP-613 type alias") + } }, Type::Dynamic(DynamicType::Todo(_)) => { self.infer_type_expression(slice); diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index f331aefa63219..a9c87f636cf8c 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -272,9 +272,6 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering (DynamicType::TodoUnpack, _) => Ordering::Less, (_, DynamicType::TodoUnpack) => Ordering::Greater, - (DynamicType::TodoTypeAlias, _) => Ordering::Less, - (_, DynamicType::TodoTypeAlias) => Ordering::Greater, - (DynamicType::Divergent(left), DynamicType::Divergent(right)) => { left.scope.cmp(&right.scope) } From 79a1305bf70e9589ef0f6bb7aa31a3c1225c7343 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 16 Oct 2025 13:48:52 +0100 Subject: [PATCH 2/2] fix assertion about the size of `Type` --- crates/ty_python_semantic/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index bbbc7ae5e21be..75021c47b3d49 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -11785,7 +11785,7 @@ pub(super) fn determine_upper_bound<'db>( // Make sure that the `Type` enum does not grow unexpectedly. #[cfg(not(debug_assertions))] #[cfg(target_pointer_width = "64")] -static_assertions::assert_eq_size!(Type, [u8; 16]); +static_assertions::assert_eq_size!(Type, [u8; 24]); #[cfg(test)] pub(crate) mod tests {