From df0c8e202dfe13a28f53b3841bd800643450c7ce Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 11 Nov 2025 17:21:43 -0800 Subject: [PATCH 1/2] [ty] support imported PEP 613 type aliases --- crates/ruff_benchmark/benches/ty_walltime.rs | 6 +- .../annotations/unsupported_special_forms.md | 3 - .../resources/mdtest/attributes.md | 4 +- .../resources/mdtest/binary/instances.md | 3 +- .../resources/mdtest/binary/integers.md | 6 +- .../resources/mdtest/call/methods.md | 4 +- .../resources/mdtest/call/open.md | 14 +- .../resources/mdtest/expression/lambda.md | 2 +- .../resources/mdtest/pep613_type_aliases.md | 126 +++++++++++++++++- .../resources/mdtest/pep695_type_aliases.md | 2 +- .../resources/mdtest/sys_version_info.md | 2 +- crates/ty_python_semantic/src/types.rs | 67 +++++----- .../src/types/class_base.rs | 5 +- .../ty_python_semantic/src/types/display.rs | 3 - .../ty_python_semantic/src/types/function.rs | 120 +---------------- .../src/types/ide_support.rs | 7 +- .../src/types/infer/builder.rs | 81 ++++++----- .../infer/builder/annotation_expression.rs | 87 ++++++++---- .../types/infer/builder/type_expression.rs | 4 + .../src/types/type_ordering.rs | 3 - 20 files changed, 297 insertions(+), 252 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index 697a0c989d..9abd6c7c83 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -143,7 +143,7 @@ static FREQTRADE: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 525, + 600, ); static PANDAS: Benchmark = Benchmark::new( @@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 3000, + 4000, ); static PYDANTIC: Benchmark = Benchmark::new( @@ -181,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY39, }, - 5000, + 7000, ); static SYMPY: Benchmark = Benchmark::new( 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 c5d737d9eb..cfc485cb53 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 @@ -12,11 +12,8 @@ P = ParamSpec("P") Ts = TypeVarTuple("Ts") R_co = TypeVar("R_co", covariant=True) -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`) 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 012a925e87..51d54fa3ae 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2208,9 +2208,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 c27e70ed76..dab5751d66 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 95561a295e..a021a15ae1 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/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index 07740c2f89..054f6d6a6a 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -598,9 +598,9 @@ from typing_extensions import Self reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__ reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__ -# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__] +# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__] reveal_type(int.__new__) -# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__] +# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__] reveal_type((42).__new__) class X: diff --git a/crates/ty_python_semantic/resources/mdtest/call/open.md b/crates/ty_python_semantic/resources/mdtest/call/open.md index 2d4552ebd8..b6cdc125ab 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/open.md +++ b/crates/ty_python_semantic/resources/mdtest/call/open.md @@ -10,13 +10,13 @@ import pickle reveal_type(open("")) # revealed: TextIOWrapper[_WrappedBuffer] reveal_type(open("", "r")) # revealed: TextIOWrapper[_WrappedBuffer] -reveal_type(open("", "rb")) # revealed: @Todo(`builtins.open` return type) +reveal_type(open("", "rb")) # revealed: BufferedReader[_BufferedReaderStream] with open("foo.pickle", "rb") as f: x = pickle.load(f) # fine def _(mode: str): - reveal_type(open("", mode)) # revealed: @Todo(`builtins.open` return type) + reveal_type(open("", mode)) # revealed: IO[Any] ``` ## `os.fdopen` @@ -29,7 +29,7 @@ import os reveal_type(os.fdopen(0)) # revealed: TextIOWrapper[_WrappedBuffer] reveal_type(os.fdopen(0, "r")) # revealed: TextIOWrapper[_WrappedBuffer] -reveal_type(os.fdopen(0, "rb")) # revealed: @Todo(`os.fdopen` return type) +reveal_type(os.fdopen(0, "rb")) # revealed: BufferedReader[_BufferedReaderStream] with os.fdopen(0, "rb") as f: x = pickle.load(f) # fine @@ -43,9 +43,9 @@ And similarly for `Path.open()`: from pathlib import Path import pickle -reveal_type(Path("").open()) # revealed: @Todo(`Path.open` return type) -reveal_type(Path("").open("r")) # revealed: @Todo(`Path.open` return type) -reveal_type(Path("").open("rb")) # revealed: @Todo(`Path.open` return type) +reveal_type(Path("").open()) # revealed: TextIOWrapper[_WrappedBuffer] +reveal_type(Path("").open("r")) # revealed: TextIOWrapper[_WrappedBuffer] +reveal_type(Path("").open("rb")) # revealed: BufferedReader[_BufferedReaderStream] with Path("foo.pickle").open("rb") as f: x = pickle.load(f) # fine @@ -61,7 +61,7 @@ import pickle reveal_type(NamedTemporaryFile()) # revealed: _TemporaryFileWrapper[bytes] reveal_type(NamedTemporaryFile("r")) # revealed: _TemporaryFileWrapper[str] -reveal_type(NamedTemporaryFile("rb")) # revealed: @Todo(`tempfile.NamedTemporaryFile` return type) +reveal_type(NamedTemporaryFile("rb")) # revealed: _TemporaryFileWrapper[bytes] with NamedTemporaryFile("rb") as f: x = pickle.load(f) # fine diff --git a/crates/ty_python_semantic/resources/mdtest/expression/lambda.md b/crates/ty_python_semantic/resources/mdtest/expression/lambda.md index b48efaad70..0538f8a7d0 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, Any] 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/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index 5272be683c..70a26ec6e6 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -1,8 +1,82 @@ # PEP 613 type aliases -## No panics +PEP 613 type aliases are simple assignment statements, annotated with `typing.TypeAlias` to mark +them as a type alias. At runtime, they behave the same as implicit type aliases. Our support for +them is currently the same as for implicit type aliases, but we don't reproduce the full +implicit-type-alias test suite here, just some particularly interesting cases. -We do not fully support PEP 613 type aliases yet. For now, just make sure that we don't panic: +## Basic + +### as `TypeAlias` + +```py +from typing import TypeAlias + +IntOrStr: TypeAlias = int | str + +def _(x: IntOrStr): + reveal_type(x) # revealed: int | str +``` + +### as `typing.TypeAlias` + +```py +import typing + +IntOrStr: typing.TypeAlias = int | str + +def _(x: IntOrStr): + reveal_type(x) # revealed: int | str +``` + +## Can be used as value + +Because PEP 613 type aliases are just annotated assignments, they can be used as values, like a +legacy type expression (and unlike a PEP 695 type alias). We might prefer this wasn't allowed, but +people do use it. + +```py +from typing import TypeAlias + +MyExc: TypeAlias = Exception + +try: + raise MyExc("error") +except MyExc as e: + reveal_type(e) # revealed: Exception +``` + +## Imported + +`alias.py`: + +```py +from typing import TypeAlias + +MyAlias: TypeAlias = int | str +``` + +`main.py`: + +```py +from alias import MyAlias + +def _(x: MyAlias): + reveal_type(x) # revealed: int | str +``` + +## String literal in RHS + +```py +from typing import TypeAlias + +IntOrStr: TypeAlias = "int | str" + +def _(x: IntOrStr): + reveal_type(x) # revealed: int | str +``` + +## Cyclic ```py from typing import TypeAlias @@ -18,6 +92,26 @@ def _(rec: RecursiveHomogeneousTuple): reveal_type(rec) # revealed: tuple[Divergent, ...] ``` +## Conditionally imported on Python < 3.10 + +```toml +[environment] +python-version = "3.9" +``` + +```py +try: + # error: [unresolved-import] + from typing import TypeAlias +except ImportError: + from typing_extensions import TypeAlias + +MyAlias: TypeAlias = int + +def _(x: MyAlias): + reveal_type(x) # revealed: int +``` + ## PEP-613 aliases in stubs are deferred Although the right-hand side of a PEP-613 alias is a value expression, inference of this value is @@ -46,7 +140,31 @@ f(stub.B()) class Unrelated: ... -# TODO: we should emit `[invalid-argument-type]` here -# (the alias is a `@Todo` because it's imported from another file) +# error: [invalid-argument-type] f(Unrelated()) ``` + +## Invalid position + +`typing.TypeAlias` must be used as the sole annotation in an annotated assignment. Use in any other +context is an error. + +```py +from typing import TypeAlias + +# error: [invalid-type-form] +def _(x: TypeAlias): + reveal_type(x) # revealed: Unknown + +# error: [invalid-type-form] +y: list[TypeAlias] = [] +``` + +## RHS is required + +```py +from typing import TypeAlias + +# error: [invalid-type-form] +Empty: TypeAlias +``` 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 167fe4025d..f0c73fc16a 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/sys_version_info.md b/crates/ty_python_semantic/resources/mdtest/sys_version_info.md index bedcdb20b9..66af7d5297 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/types.rs b/crates/ty_python_semantic/src/types.rs index 518c0f1c59..3134f34fad 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4369,11 +4369,6 @@ impl<'db> Type<'db> { Type::KnownBoundMethod(KnownBoundMethodType::StrStartswith(literal)), ) .into(), - Type::NominalInstance(instance) - if instance.has_known_class(db, KnownClass::Path) && name == "open" => - { - Place::bound(Type::KnownBoundMethod(KnownBoundMethodType::PathOpen)).into() - } Type::ClassLiteral(class) if name == "range" && class.is_known(db, KnownClass::ConstraintSet) => @@ -6737,6 +6732,7 @@ impl<'db> Type<'db> { Ok(ty.inner(db).to_meta_type(db)) } + KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)), }, Type::SpecialForm(special_form) => match special_form { @@ -6791,7 +6787,15 @@ impl<'db> Type<'db> { Ok(typing_self(db, scope_id, typevar_binding_context, class).unwrap_or(*self)) } - SpecialFormType::TypeAlias => Ok(Type::Dynamic(DynamicType::TodoTypeAlias)), + // We ensure that `typing.TypeAlias` used in the expected position (annotating an + // annotated assignment statement) doesn't reach here. Using it in any other type + // expression is an error. + SpecialFormType::TypeAlias => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::TypeAlias + ], + fallback_type: Type::unknown(), + }), SpecialFormType::TypedDict => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec_inline![ InvalidTypeExpression::TypedDict @@ -7269,7 +7273,6 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::KnownBoundMethod( KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -7429,7 +7432,6 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::KnownBoundMethod( KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -7980,6 +7982,9 @@ pub enum KnownInstanceType<'db> { /// An instance of `typing.GenericAlias` representing a `type[...]` expression. TypeGenericAlias(InternedType<'db>), + /// A literal string which is the right-hand side of a PEP 613 `TypeAlias`. + LiteralStringAlias(InternedType<'db>), + /// An identity callable created with `typing.NewType(name, base)`, which behaves like a /// subtype of `base` in type expressions. See the `struct NewType` payload for an example. NewType(NewType<'db>), @@ -8016,7 +8021,8 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( } KnownInstanceType::Literal(ty) | KnownInstanceType::Annotated(ty) - | KnownInstanceType::TypeGenericAlias(ty) => { + | KnownInstanceType::TypeGenericAlias(ty) + | KnownInstanceType::LiteralStringAlias(ty) => { visitor.visit_type(db, ty.inner(db)); } KnownInstanceType::NewType(newtype) => { @@ -8064,6 +8070,9 @@ impl<'db> KnownInstanceType<'db> { Self::Literal(ty) => Self::Literal(ty.normalized_impl(db, visitor)), Self::Annotated(ty) => Self::Annotated(ty.normalized_impl(db, visitor)), Self::TypeGenericAlias(ty) => Self::TypeGenericAlias(ty.normalized_impl(db, visitor)), + Self::LiteralStringAlias(ty) => { + Self::LiteralStringAlias(ty.normalized_impl(db, visitor)) + } Self::NewType(newtype) => Self::NewType( newtype .map_base_class_type(db, |class_type| class_type.normalized_impl(db, visitor)), @@ -8089,6 +8098,7 @@ impl<'db> KnownInstanceType<'db> { Self::Literal(_) | Self::Annotated(_) | Self::TypeGenericAlias(_) => { KnownClass::GenericAlias } + Self::LiteralStringAlias(_) => KnownClass::Str, Self::NewType(_) => KnownClass::NewType, } } @@ -8175,6 +8185,7 @@ impl<'db> KnownInstanceType<'db> { f.write_str("") } KnownInstanceType::TypeGenericAlias(_) => f.write_str("GenericAlias"), + KnownInstanceType::LiteralStringAlias(_) => f.write_str("str"), KnownInstanceType::NewType(declaration) => { write!(f, "", declaration.name(self.db)) } @@ -8216,9 +8227,6 @@ pub enum DynamicType<'db> { /// /// This variant should be created with the `todo_type!` macro. Todo(TodoType), - /// 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. @@ -8250,13 +8258,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"), } } @@ -8415,6 +8416,9 @@ enum InvalidTypeExpression<'db> { ConstraintSet, /// Same for `typing.TypedDict` TypedDict, + /// Same for `typing.TypeAlias`, anywhere except for as the sole annotation on an annotated + /// assignment + TypeAlias, /// Type qualifiers are always invalid in *type expressions*, /// but these ones are okay with 0 arguments in *annotation expressions* TypeQualifier(SpecialFormType), @@ -8470,6 +8474,11 @@ impl<'db> InvalidTypeExpression<'db> { "The special form `typing.TypedDict` is not allowed in type expressions. \ Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?") } + InvalidTypeExpression::TypeAlias => { + f.write_str( + "`typing.TypeAlias` is only allowed as the sole annotation on an annotated assignment", + ) + } InvalidTypeExpression::TypeQualifier(qualifier) => write!( f, "Type qualifier `{qualifier}` is not allowed in type expressions \ @@ -10910,8 +10919,6 @@ pub enum KnownBoundMethodType<'db> { /// this allows us to understand statically known branches for common tests such as /// `if sys.platform.startswith("freebsd")`. StrStartswith(StringLiteralType<'db>), - /// Method wrapper for `Path.open`, - PathOpen, // ConstraintSet methods ConstraintSetRange, @@ -10943,8 +10950,7 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size KnownBoundMethodType::StrStartswith(string_literal) => { visitor.visit_type(db, Type::StringLiteral(string_literal)); } - KnownBoundMethodType::PathOpen - | KnownBoundMethodType::ConstraintSetRange + KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) @@ -11001,8 +11007,7 @@ impl<'db> KnownBoundMethodType<'db> { ConstraintSet::from(self == other) } - (KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen) - | ( + ( KnownBoundMethodType::ConstraintSetRange, KnownBoundMethodType::ConstraintSetRange, ) @@ -11033,7 +11038,6 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -11045,7 +11049,6 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -11087,8 +11090,7 @@ impl<'db> KnownBoundMethodType<'db> { ConstraintSet::from(self == other) } - (KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen) - | ( + ( KnownBoundMethodType::ConstraintSetRange, KnownBoundMethodType::ConstraintSetRange, ) @@ -11122,7 +11124,6 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -11134,7 +11135,6 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -11160,7 +11160,6 @@ impl<'db> KnownBoundMethodType<'db> { KnownBoundMethodType::PropertyDunderSet(property.normalized_impl(db, visitor)) } KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -11178,7 +11177,6 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderSet(_) => KnownClass::MethodWrapperType, KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType, - KnownBoundMethodType::PathOpen => KnownClass::MethodType, KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -11283,9 +11281,6 @@ impl<'db> KnownBoundMethodType<'db> { Some(KnownClass::Bool.to_instance(db)), ))) } - KnownBoundMethodType::PathOpen => { - Either::Right(std::iter::once(Signature::todo("`Path.open` return type"))) - } KnownBoundMethodType::ConstraintSetRange => { Either::Right(std::iter::once(Signature::new( diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 8015dfcf00..ca7322a9d4 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -48,9 +48,7 @@ impl<'db> ClassBase<'db> { ClassBase::Class(class) => class.name(db), ClassBase::Dynamic(DynamicType::Any) => "Any", ClassBase::Dynamic(DynamicType::Unknown) => "Unknown", - ClassBase::Dynamic( - DynamicType::Todo(_) | DynamicType::TodoTypeAlias | DynamicType::TodoUnpack, - ) => "@Todo", + ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack) => "@Todo", ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent", ClassBase::Protocol => "Protocol", ClassBase::Generic => "Generic", @@ -176,6 +174,7 @@ impl<'db> ClassBase<'db> { | KnownInstanceType::ConstraintSet(_) | KnownInstanceType::UnionType(_) | KnownInstanceType::Literal(_) + | KnownInstanceType::LiteralStringAlias(_) // A class inheriting from a newtype would make intuitive sense, but newtype // wrappers are just identity callables at runtime, so this sort of inheritance // doesn't work and isn't allowed. diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index b8a8a05ac4..f94e9f119d 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -520,9 +520,6 @@ impl Display for DisplayRepresentation<'_> { Type::KnownBoundMethod(KnownBoundMethodType::StrStartswith(_)) => { f.write_str("") } - Type::KnownBoundMethod(KnownBoundMethodType::PathOpen) => { - f.write_str("bound method `Path.open`") - } Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetRange) => { f.write_str("bound method `ConstraintSet.range`") } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 085bbaa60f..ef1bc21221 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -83,7 +83,7 @@ use crate::types::{ ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, NormalizedVisitor, SpecialFormType, Truthiness, Type, TypeContext, TypeMapping, TypeRelation, - UnionBuilder, binding_type, todo_type, walk_signature, + UnionBuilder, binding_type, walk_signature, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -1152,70 +1152,6 @@ fn is_instance_truthiness<'db>( } } -/// Return true, if the type passed as `mode` would require us to pick a non-trivial overload of -/// `builtins.open` / `os.fdopen` / `Path.open`. -fn is_mode_with_nontrivial_return_type<'db>(db: &'db dyn Db, mode: Type<'db>) -> bool { - // Return true for any mode that doesn't match typeshed's - // `OpenTextMode` type alias (). - mode.as_string_literal().is_none_or(|mode| { - !matches!( - mode.value(db), - "r+" | "+r" - | "rt+" - | "r+t" - | "+rt" - | "tr+" - | "t+r" - | "+tr" - | "w+" - | "+w" - | "wt+" - | "w+t" - | "+wt" - | "tw+" - | "t+w" - | "+tw" - | "a+" - | "+a" - | "at+" - | "a+t" - | "+at" - | "ta+" - | "t+a" - | "+ta" - | "x+" - | "+x" - | "xt+" - | "x+t" - | "+xt" - | "tx+" - | "t+x" - | "+tx" - | "w" - | "wt" - | "tw" - | "a" - | "at" - | "ta" - | "x" - | "xt" - | "tx" - | "r" - | "rt" - | "tr" - | "U" - | "rU" - | "Ur" - | "rtU" - | "rUt" - | "Urt" - | "trU" - | "tUr" - | "Utr" - ) - }) -} - fn signature_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, @@ -1268,16 +1204,6 @@ pub enum KnownFunction { DunderImport, /// `importlib.import_module`, which returns the submodule. ImportModule, - /// `builtins.open` - Open, - - /// `os.fdopen` - Fdopen, - - /// `tempfile.NamedTemporaryFile` - #[strum(serialize = "NamedTemporaryFile")] - NamedTemporaryFile, - /// `typing(_extensions).final` Final, /// `typing(_extensions).disjoint_base` @@ -1376,7 +1302,6 @@ impl KnownFunction { | Self::HasAttr | Self::Len | Self::Repr - | Self::Open | Self::DunderImport => module.is_builtins(), Self::AssertType | Self::AssertNever @@ -1396,12 +1321,6 @@ impl KnownFunction { Self::AbstractMethod => { matches!(module, KnownModule::Abc) } - Self::Fdopen => { - matches!(module, KnownModule::Os) - } - Self::NamedTemporaryFile => { - matches!(module, KnownModule::Tempfile) - } Self::Dataclass | Self::Field => { matches!(module, KnownModule::Dataclasses) } @@ -1871,38 +1790,6 @@ impl KnownFunction { overload.set_return_type(Type::module_literal(db, file, module)); } - - KnownFunction::Open => { - // TODO: Temporary special-casing for `builtins.open` to avoid an excessive number of - // false positives in lieu of proper support for PEP-613 type aliases. - if let [_, Some(mode), ..] = parameter_types - && is_mode_with_nontrivial_return_type(db, *mode) - { - overload.set_return_type(todo_type!("`builtins.open` return type")); - } - } - - KnownFunction::Fdopen => { - // TODO: Temporary special-casing for `os.fdopen` to avoid an excessive number of - // false positives in lieu of proper support for PEP-613 type aliases. - if let [_, Some(mode), ..] = parameter_types - && is_mode_with_nontrivial_return_type(db, *mode) - { - overload.set_return_type(todo_type!("`os.fdopen` return type")); - } - } - - KnownFunction::NamedTemporaryFile => { - // TODO: Temporary special-casing for `tempfile.NamedTemporaryFile` to avoid an excessive number of - // false positives in lieu of proper support for PEP-613 type aliases. - if let [Some(mode), ..] = parameter_types - && is_mode_with_nontrivial_return_type(db, *mode) - { - overload - .set_return_type(todo_type!("`tempfile.NamedTemporaryFile` return type")); - } - } - _ => {} } } @@ -1929,15 +1816,10 @@ pub(crate) mod tests { | KnownFunction::IsInstance | KnownFunction::HasAttr | KnownFunction::IsSubclass - | KnownFunction::Open | KnownFunction::DunderImport => KnownModule::Builtins, KnownFunction::AbstractMethod => KnownModule::Abc, - KnownFunction::Fdopen => KnownModule::Os, - - KnownFunction::NamedTemporaryFile => KnownModule::Tempfile, - KnownFunction::Dataclass | KnownFunction::Field => KnownModule::Dataclasses, KnownFunction::GetattrStatic => KnownModule::Inspect, diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index d57869825a..ac91be6087 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -15,7 +15,7 @@ use crate::types::generics::Specialization; use crate::types::signatures::Signature; use crate::types::{CallDunderError, UnionType}; use crate::types::{ - ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type, TypeContext, + ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext, TypeVarBoundOrConstraints, class::CodeGeneratorKind, }; use crate::{Db, HasType, NameKind, SemanticModel}; @@ -299,9 +299,10 @@ impl<'db> AllMembers<'db> { Type::KnownInstance( KnownInstanceType::TypeVar(_) | KnownInstanceType::TypeAliasType(_) - | KnownInstanceType::UnionType(_), + | KnownInstanceType::UnionType(_) + | KnownInstanceType::Literal(_) + | KnownInstanceType::Annotated(_), ) => 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 b95b86db09..ac5b27c160 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5390,7 +5390,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let target = assignment.target(self.module()); let value = assignment.value(self.module()); - let mut declared = self.infer_annotation_expression( + let mut declared = self.infer_annotation_expression_allow_pep_613( annotation, DeferredExpressionState::from(self.defer_annotations()), ); @@ -5442,6 +5442,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { declared.inner = Type::BooleanLiteral(true); } + // Check if this is a PEP 613 `TypeAlias`. (This must come below the SpecialForm handling + // immediately below, since that can overwrite the type to be `TypeAlias`.) + let is_pep_613_type_alias = matches!( + declared.inner_type(), + Type::SpecialForm(SpecialFormType::TypeAlias) + ); + // Handle various singletons. if let Some(name_expr) = target.as_name_expr() { if let Some(special_form) = @@ -5487,20 +5494,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // We defer the r.h.s. of PEP-613 `TypeAlias` assignments in stub files. - let declared_type = declared.inner_type(); let previous_deferred_state = self.deferred_state; - if matches!( - declared_type, - Type::SpecialForm(SpecialFormType::TypeAlias) - | Type::Dynamic(DynamicType::TodoTypeAlias) - ) && self.in_stub() - { + if is_pep_613_type_alias && self.in_stub() { self.deferred_state = DeferredExpressionState::Deferred; } - let inferred_ty = self - .infer_maybe_standalone_expression(value, TypeContext::new(Some(declared_type))); + let inferred_ty = if is_pep_613_type_alias && value.is_string_literal_expr() { + let aliased_type = self.infer_type_expression(value); + Type::KnownInstance(KnownInstanceType::LiteralStringAlias(InternedType::new( + self.db(), + aliased_type, + ))) + } else { + self.infer_maybe_standalone_expression( + value, + TypeContext::new(Some(declared.inner_type())), + ) + }; self.deferred_state = previous_deferred_state; @@ -5517,17 +5528,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { inferred_ty }; - self.add_declaration_with_binding( - target.into(), - definition, - &DeclaredAndInferredType::MightBeDifferent { - declared_ty: declared, - inferred_ty, - }, - ); + if is_pep_613_type_alias { + self.add_declaration_with_binding( + target.into(), + definition, + &DeclaredAndInferredType::AreTheSame(TypeAndQualifiers::declared(inferred_ty)), + ); + } else { + self.add_declaration_with_binding( + target.into(), + definition, + &DeclaredAndInferredType::MightBeDifferent { + declared_ty: declared, + inferred_ty, + }, + ); + } self.store_expression_type(target, inferred_ty); } else { + if is_pep_613_type_alias { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, annotation) { + builder.into_diagnostic( + "`TypeAlias` must be assigned a value in annotated assignments", + ); + } + declared.inner = Type::unknown(); + } if self.in_stub() { self.add_declaration_with_binding( target.into(), @@ -9286,20 +9313,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (unknown @ Type::Dynamic(DynamicType::Unknown), _, _) | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), - ( - todo @ Type::Dynamic( - DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoTypeAlias, - ), - _, - _, - ) - | ( - _, - todo @ Type::Dynamic( - DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoTypeAlias, - ), - _, - ) => Some(todo), + (todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack), _, _) + | (_, todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack), _) => { + Some(todo) + } (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 5e1f852695..6550f2b950 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -18,21 +18,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { annotation: &ast::Expr, deferred_state: DeferredExpressionState, ) -> TypeAndQualifiers<'db> { - // `DeferredExpressionState::InStringAnnotation` takes precedence over other deferred states. - // However, if it's not a stringified annotation, we must still ensure that annotation expressions - // are always deferred in stub files. - let state = if deferred_state.in_string_annotation() { - deferred_state - } else if self.in_stub() { - DeferredExpressionState::Deferred - } else { - deferred_state - }; + self.infer_annotation_expression_inner(annotation, deferred_state, false) + } - let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state); - let annotation_ty = self.infer_annotation_expression_impl(annotation); - self.deferred_state = previous_deferred_state; - annotation_ty + /// Infer the type of an annotation expression with the given [`DeferredExpressionState`], + /// allowing a PEP 613 `typing.TypeAlias` annotation. + pub(super) fn infer_annotation_expression_allow_pep_613( + &mut self, + annotation: &ast::Expr, + deferred_state: DeferredExpressionState, + ) -> TypeAndQualifiers<'db> { + self.infer_annotation_expression_inner(annotation, deferred_state, true) } /// Similar to [`infer_annotation_expression`], but accepts an optional annotation expression @@ -47,17 +43,42 @@ impl<'db> TypeInferenceBuilder<'db, '_> { annotation.map(|expr| self.infer_annotation_expression(expr, deferred_state)) } + fn infer_annotation_expression_inner( + &mut self, + annotation: &ast::Expr, + deferred_state: DeferredExpressionState, + allow_pep_613: bool, + ) -> TypeAndQualifiers<'db> { + // `DeferredExpressionState::InStringAnnotation` takes precedence over other deferred states. + // However, if it's not a stringified annotation, we must still ensure that annotation expressions + // are always deferred in stub files. + let state = if deferred_state.in_string_annotation() { + deferred_state + } else if self.in_stub() { + DeferredExpressionState::Deferred + } else { + deferred_state + }; + + let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state); + let annotation_ty = self.infer_annotation_expression_impl(annotation, allow_pep_613); + self.deferred_state = previous_deferred_state; + annotation_ty + } + /// Implementation of [`infer_annotation_expression`]. /// /// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression fn infer_annotation_expression_impl( &mut self, annotation: &ast::Expr, + allow_pep_613: bool, ) -> TypeAndQualifiers<'db> { fn infer_name_or_attribute<'db>( ty: Type<'db>, annotation: &ast::Expr, builder: &TypeInferenceBuilder<'db, '_>, + allow_pep_613: bool, ) -> TypeAndQualifiers<'db> { match ty { Type::SpecialForm(SpecialFormType::ClassVar) => TypeAndQualifiers::new( @@ -85,6 +106,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { TypeOrigin::Declared, TypeQualifiers::READ_ONLY, ), + Type::SpecialForm(SpecialFormType::TypeAlias) if allow_pep_613 => { + TypeAndQualifiers::declared(ty) + } + // Conditional import of `typing.TypeAlias` or `typing_extensions.TypeAlias` on a + // Python version where the former doesn't exist. + Type::Union(union) + if allow_pep_613 + && union.elements(builder.db()).iter().all(|ty| { + matches!( + ty, + Type::SpecialForm(SpecialFormType::TypeAlias) | Type::Dynamic(_) + ) + }) => + { + TypeAndQualifiers::declared(Type::SpecialForm(SpecialFormType::TypeAlias)) + } Type::ClassLiteral(class) if class.is_known(builder.db(), KnownClass::InitVar) => { if let Some(builder) = builder.context.report_lint(&INVALID_TYPE_FORM, annotation) @@ -148,6 +185,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_attribute_expression(attribute), annotation, self, + allow_pep_613, ), ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()), ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared( @@ -156,9 +194,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }, ast::Expr::Name(name) => match name.ctx { - ast::ExprContext::Load => { - infer_name_or_attribute(self.infer_name_expression(name), annotation, self) - } + ast::ExprContext::Load => infer_name_or_attribute( + self.infer_name_expression(name), + annotation, + self, + allow_pep_613, + ), ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()), ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared( todo_type!("Name expression annotation in Store/Del context"), @@ -189,7 +230,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } let inner_annotation_ty = - self.infer_annotation_expression_impl(inner_annotation); + self.infer_annotation_expression_impl(inner_annotation, false); self.store_expression_type(slice, inner_annotation_ty.inner_type()); inner_annotation_ty @@ -202,7 +243,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } else { report_invalid_arguments_to_annotated(&self.context, subscript); - self.infer_annotation_expression_impl(slice) + self.infer_annotation_expression_impl(slice, false) } } Type::SpecialForm( @@ -220,7 +261,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let num_arguments = arguments.len(); let type_and_qualifiers = if num_arguments == 1 { let mut type_and_qualifiers = - self.infer_annotation_expression_impl(slice); + self.infer_annotation_expression_impl(slice, false); match type_qualifier { SpecialFormType::ClassVar => { @@ -243,7 +284,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { type_and_qualifiers } else { for element in arguments { - self.infer_annotation_expression_impl(element); + self.infer_annotation_expression_impl(element, false); } if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) @@ -269,12 +310,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let num_arguments = arguments.len(); let type_and_qualifiers = if num_arguments == 1 { let mut type_and_qualifiers = - self.infer_annotation_expression_impl(slice); + self.infer_annotation_expression_impl(slice, false); type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR); type_and_qualifiers } else { for element in arguments { - self.infer_annotation_expression_impl(element); + self.infer_annotation_expression_impl(element, false); } if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) 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 c33c058892..9133838f51 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 @@ -825,6 +825,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_type_expression(slice); todo_type!("Generic manual PEP-695 type alias") } + KnownInstanceType::LiteralStringAlias(_) => { + self.infer_type_expression(slice); + todo_type!("Generic stringified PEP-613 type alias") + } KnownInstanceType::UnionType(_) => { self.infer_type_expression(slice); todo_type!("Generic specialization of types.UnionType") diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 946b6173a2..1b9b00a8fd 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -269,9 +269,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 f6b892c5aac3f4f547429c4a706fc27cd9471154 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 15 Nov 2025 15:04:04 -0800 Subject: [PATCH 2/2] specialize interpretation of PEP 604 unions --- .../resources/mdtest/pep613_type_aliases.md | 42 +++++++++++++ crates/ty_python_semantic/src/types.rs | 4 ++ .../src/types/infer/builder.rs | 61 +++++++++++++++++-- .../types/infer/builder/type_expression.rs | 2 +- 4 files changed, 102 insertions(+), 7 deletions(-) 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 70a26ec6e6..aa6e0e837a 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -46,6 +46,48 @@ except MyExc as e: reveal_type(e) # revealed: Exception ``` +## Unknown type in PEP 604 union + +If we run into an unexpected type in a PEP 604 union in the RHS of a PEP 613 type alias, we still +understand it as a union type, just with an unknown element. + +```py +from typing import TypeAlias +from nonexistent import unknown_type # error: [unresolved-import] + +MyAlias: TypeAlias = int | unknown_type | str + +def _(x: MyAlias): + reveal_type(x) # revealed: int | Unknown | str +``` + +## Callable type in union + +```py +from typing import TypeAlias, Callable + +MyAlias: TypeAlias = int | Callable[[str], int] + +def _(x: MyAlias): + # TODO: int | (str) -> int + reveal_type(x) # revealed: int | @Todo(Inference of subscript on special form) +``` + +## Subscripted generic alias in union + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T") + +Alias1: TypeAlias = list[T] | set[T] +MyAlias: TypeAlias = int | Alias1[str] + +def _(x: MyAlias): + # TODO: int | list[str] | set[str] + reveal_type(x) # revealed: int | @Todo(Specialization of union type alias) +``` + ## Imported `alias.py`: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3134f34fad..0c07afdd5f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -849,6 +849,10 @@ impl<'db> Type<'db> { .is_some() } + fn is_typealias_special_form(&self) -> bool { + matches!(self, Type::SpecialForm(SpecialFormType::TypeAlias)) + } + /// Return true if this type overrides __eq__ or __ne__ methods fn overrides_equality(&self, db: &'db dyn Db) -> bool { let check_dunder = |dunder_name, allowed_return_value| { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ac5b27c160..6615a4c36d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5444,10 +5444,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Check if this is a PEP 613 `TypeAlias`. (This must come below the SpecialForm handling // immediately below, since that can overwrite the type to be `TypeAlias`.) - let is_pep_613_type_alias = matches!( - declared.inner_type(), - Type::SpecialForm(SpecialFormType::TypeAlias) - ); + let is_pep_613_type_alias = declared.inner_type().is_typealias_special_form(); // Handle various singletons. if let Some(name_expr) = target.as_name_expr() { @@ -6926,7 +6923,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::Expr::Name(name) => self.infer_name_expression(name), ast::Expr::Attribute(attribute) => self.infer_attribute_expression(attribute), ast::Expr::UnaryOp(unary_op) => self.infer_unary_expression(unary_op), - ast::Expr::BinOp(binary) => self.infer_binary_expression(binary), + ast::Expr::BinOp(binary) => self.infer_binary_expression(binary, tcx), ast::Expr::BoolOp(bool_op) => self.infer_boolean_expression(bool_op), ast::Expr::Compare(compare) => self.infer_compare_expression(compare), ast::Expr::Subscript(subscript) => self.infer_subscript_expression(subscript), @@ -9200,7 +9197,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - fn infer_binary_expression(&mut self, binary: &ast::ExprBinOp) -> Type<'db> { + fn infer_binary_expression( + &mut self, + binary: &ast::ExprBinOp, + tcx: TypeContext<'db>, + ) -> Type<'db> { + if tcx + .annotation + .is_some_and(|ty| ty.is_typealias_special_form()) + { + return self.infer_pep_604_union_type_alias(binary, tcx); + } + let ast::ExprBinOp { left, op, @@ -9238,6 +9246,44 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) } + fn infer_pep_604_union_type_alias( + &mut self, + node: &ast::ExprBinOp, + tcx: TypeContext<'db>, + ) -> Type<'db> { + let ast::ExprBinOp { + left, + op, + right, + range: _, + node_index: _, + } = node; + + if *op != ast::Operator::BitOr { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) { + let mut diag = builder.into_diagnostic(format_args!( + "Invalid binary operator `{}` in type alias", + op.as_str() + )); + diag.info("Did you mean to use `|`?"); + } + return Type::unknown(); + } + + let left_ty = self.infer_expression(left, tcx); + let right_ty = self.infer_expression(right, tcx); + + if left_ty.is_equivalent_to(self.db(), right_ty) { + left_ty + } else { + Type::KnownInstance(KnownInstanceType::UnionType(InternedTypes::from_elements( + self.db(), + [left_ty, right_ty], + InferredAs::ValueExpression, + ))) + } + } + fn infer_binary_expression_type( &mut self, node: AnyNodeRef<'_>, @@ -10916,6 +10962,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(Type::from) .unwrap_or_else(Type::unknown); } + Type::KnownInstance(KnownInstanceType::UnionType(_)) => { + return todo_type!("Specialization of union type alias"); + } _ => {} } 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 9133838f51..3c1037d952 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 @@ -147,7 +147,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } // anything else is an invalid annotation: op => { - self.infer_binary_expression(binary); + self.infer_binary_expression(binary, TypeContext::default()); if let Some(mut diag) = self.report_invalid_type_expression( expression, format_args!(