From eb7c911a327fd5ebc5fe4b9f80a66737f757ba12 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 21 Nov 2025 08:48:32 +0100 Subject: [PATCH 01/25] [ty] Generic implicit types aliases --- .../resources/mdtest/async.md | 5 +- .../resources/mdtest/implicit_type_aliases.md | 102 ++++++++------- crates/ty_python_semantic/src/types.rs | 117 +++++++++++++++--- .../src/types/infer/builder.rs | 6 + .../types/infer/builder/type_expression.rs | 81 ++++++++---- 5 files changed, 217 insertions(+), 94 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/async.md b/crates/ty_python_semantic/resources/mdtest/async.md index 0d57f4d8f8ce8..c46d0f86ba8a6 100644 --- a/crates/ty_python_semantic/resources/mdtest/async.md +++ b/crates/ty_python_semantic/resources/mdtest/async.md @@ -79,9 +79,8 @@ async def main(): task("B"), ) - # TODO: these should be `int` - reveal_type(a) # revealed: Unknown - reveal_type(b) # revealed: Unknown + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int ``` ## Under the hood 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 4b28f150bccd8..6a3a9ad37d5e4 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -388,6 +388,8 @@ ListOrTuple = list[T] | tuple[T, ...] ListOrTupleLegacy = Union[list[T], tuple[T, ...]] MyCallable = Callable[P, T] AnnotatedType = Annotated[T, "tag"] +TransparentAlias = T +MyOptional = T | None # TODO: Consider displaying this as ``, … instead? (and similar for some others below) reveal_type(MyList) # revealed: @@ -400,43 +402,40 @@ reveal_type(ListOrTuple) # revealed: types.UnionType reveal_type(ListOrTupleLegacy) # revealed: types.UnionType reveal_type(MyCallable) # revealed: GenericAlias reveal_type(AnnotatedType) # revealed: +reveal_type(TransparentAlias) # revealed: typing.TypeVar +reveal_type(MyOptional) # revealed: types.UnionType def _( list_of_ints: MyList[int], dict_str_to_int: MyDict[str, int], - # TODO: no error here - # error: [invalid-type-form] "`typing.TypeVar` is not a generic class" subclass_of_int: MyType[int], int_and_str: IntAndType[str], pair_of_ints: Pair[int], int_and_bytes: Sum[int, bytes], list_or_tuple: ListOrTuple[int], list_or_tuple_legacy: ListOrTupleLegacy[int], - # TODO: no error here + # TODO: no errors here # error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[str, bytes]`?" + # error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2" my_callable: MyCallable[[str, bytes], int], annotated_int: AnnotatedType[int], + transparent_alias: TransparentAlias[int], + optional_int: MyOptional[int], ): - # TODO: This should be `list[int]` - reveal_type(list_of_ints) # revealed: @Todo(specialized generic alias in type expression) - # TODO: This should be `dict[str, int]` - reveal_type(dict_str_to_int) # revealed: @Todo(specialized generic alias in type expression) - # TODO: This should be `type[int]` - reveal_type(subclass_of_int) # revealed: Unknown - # TODO: This should be `tuple[int, str]` - reveal_type(int_and_str) # revealed: @Todo(specialized generic alias in type expression) - # TODO: This should be `tuple[int, int]` - reveal_type(pair_of_ints) # revealed: @Todo(specialized generic alias in type expression) - # TODO: This should be `tuple[int, bytes]` - reveal_type(int_and_bytes) # revealed: @Todo(specialized generic alias in type expression) - # TODO: This should be `list[int] | tuple[int, ...]` - reveal_type(list_or_tuple) # revealed: @Todo(Generic specialization of types.UnionType) - # TODO: This should be `list[int] | tuple[int, ...]` - reveal_type(list_or_tuple_legacy) # revealed: @Todo(Generic specialization of types.UnionType) + reveal_type(list_of_ints) # revealed: list[int] + reveal_type(dict_str_to_int) # revealed: dict[str, int] + reveal_type(subclass_of_int) # revealed: type[int] + reveal_type(int_and_str) # revealed: tuple[int, str] + reveal_type(pair_of_ints) # revealed: tuple[int, int] + reveal_type(int_and_bytes) # revealed: tuple[int, bytes] + reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...] + reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...] + reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...] # TODO: This should be `(str, bytes) -> int` - reveal_type(my_callable) # revealed: @Todo(Generic specialization of typing.Callable) - # TODO: This should be `int` - reveal_type(annotated_int) # revealed: @Todo(Generic specialization of typing.Annotated) + reveal_type(my_callable) # revealed: Unknown + reveal_type(annotated_int) # revealed: int + reveal_type(transparent_alias) # revealed: int + reveal_type(optional_int) # revealed: int | None ``` Generic implicit type aliases can be partially specialized: @@ -446,15 +445,12 @@ U = TypeVar("U") DictStrTo = MyDict[str, U] -reveal_type(DictStrTo) # revealed: GenericAlias +reveal_type(DictStrTo) # revealed: def _( - # TODO: No error here - # error: [invalid-type-form] "Invalid subscript of object of type `GenericAlias` in type expression" dict_str_to_int: DictStrTo[int], ): - # TODO: This should be `dict[str, int]` - reveal_type(dict_str_to_int) # revealed: Unknown + reveal_type(dict_str_to_int) # revealed: dict[str, int] ``` Using specializations of generic implicit type aliases in other implicit type aliases works as @@ -465,25 +461,31 @@ IntsOrNone = MyList[int] | None IntsOrStrs = Pair[int] | Pair[str] ListOfPairs = MyList[Pair[str]] -reveal_type(IntsOrNone) # revealed: UnionType -reveal_type(IntsOrStrs) # revealed: UnionType -reveal_type(ListOfPairs) # revealed: GenericAlias +reveal_type(IntsOrNone) # revealed: types.UnionType +reveal_type(IntsOrStrs) # revealed: types.UnionType +reveal_type(ListOfPairs) # revealed: def _( - # TODO: This should not be an error - # error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression" ints_or_none: IntsOrNone, - # TODO: This should not be an error - # error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression" ints_or_strs: IntsOrStrs, list_of_pairs: ListOfPairs, ): - # TODO: This should be `list[int] | None` - reveal_type(ints_or_none) # revealed: Unknown - # TODO: This should be `tuple[int, int] | tuple[str, str]` - reveal_type(ints_or_strs) # revealed: Unknown - # TODO: This should be `list[tuple[str, str]]` - reveal_type(list_of_pairs) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions) + reveal_type(ints_or_none) # revealed: list[int] | None + reveal_type(ints_or_strs) # revealed: tuple[int, int] | tuple[str, str] + reveal_type(list_of_pairs) # revealed: list[tuple[str, str]] +``` + +A generic implicit type alias can also be used in another generic implicit type alias: + +```py +MyOtherList = MyList[T] + +reveal_type(MyOtherList) # revealed: + +def _( + list_of_ints: MyOtherList[int], +): + reveal_type(list_of_ints) # revealed: list[int] ``` If a generic implicit type alias is used unspecialized in a type expression, we treat it as an @@ -522,8 +524,6 @@ reveal_mro(Derived1) GenericBaseAlias = GenericBase[T] -# TODO: No error here -# error: [non-subscriptable] "Cannot subscript object of type `` with no `__class_getitem__` method" class Derived2(GenericBaseAlias[int]): pass ``` @@ -533,10 +533,9 @@ A generic alias that is already fully specialized cannot be specialized again: ```py ListOfInts = list[int] -# TODO: this should be an error +# error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 1" def _(doubly_specialized: ListOfInts[int]): - # TODO: this should be `Unknown` - reveal_type(doubly_specialized) # revealed: @Todo(specialized generic alias in type expression) + reveal_type(doubly_specialized) # revealed: Unknown ``` Specializing a generic implicit type alias with an incorrect number of type arguments also results @@ -544,15 +543,13 @@ in an error: ```py def _( - # TODO: this should be an error + # error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2" list_too_many_args: MyList[int, str], - # TODO: this should be an error + # error: [missing-argument] "No argument provided for required parameter `U`" dict_too_few_args: MyDict[int], ): - # TODO: this should be `Unknown` - reveal_type(list_too_many_args) # revealed: @Todo(specialized generic alias in type expression) - # TODO: this should be `Unknown` - reveal_type(dict_too_few_args) # revealed: @Todo(specialized generic alias in type expression) + reveal_type(list_too_many_args) # revealed: Unknown + reveal_type(dict_too_few_args) # revealed: Unknown ``` ## `Literal`s @@ -642,8 +639,7 @@ Deprecated = Annotated[T, "deprecated attribute"] class C: old: Deprecated[int] -# TODO: Should be `int` -reveal_type(C().old) # revealed: @Todo(Generic specialization of typing.Annotated) +reveal_type(C().old) # revealed: int ``` If the metadata argument is missing, we emit an error (because this code fails at runtime), but diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 94ff50f161a33..c42d28ef9df35 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7578,18 +7578,71 @@ impl<'db> Type<'db> { match self { Type::TypeVar(bound_typevar) => bound_typevar.apply_type_mapping_impl(db, type_mapping, visitor), - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => match type_mapping { - TypeMapping::BindLegacyTypevars(binding_context) => { - Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context)) + Type::KnownInstance(known_instance) => match known_instance { + KnownInstanceType::TypeVar(typevar) => { + match type_mapping { + TypeMapping::BindLegacyTypevars(binding_context) => { + Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context)) + } + TypeMapping::Specialization(_) | + TypeMapping::PartialSpecialization(_) | + TypeMapping::PromoteLiterals(_) | + TypeMapping::BindSelf(_) | + TypeMapping::ReplaceSelf { .. } | + TypeMapping::Materialize(_) | + TypeMapping::ReplaceParameterDefaults | + TypeMapping::EagerExpansion => self, + } } - TypeMapping::Specialization(_) | - TypeMapping::PartialSpecialization(_) | - TypeMapping::PromoteLiterals(_) | - TypeMapping::BindSelf(_) | - TypeMapping::ReplaceSelf { .. } | - TypeMapping::Materialize(_) | - TypeMapping::ReplaceParameterDefaults | - TypeMapping::EagerExpansion => self, + KnownInstanceType::UnionType(instance) => { + if let Ok(union_type) = instance.union_type(db) { + Type::KnownInstance(KnownInstanceType::UnionType( + UnionTypeInstance::new( + db, + instance._value_expr_types(db), + Ok(union_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + ) + ))) + } else { + self + } + }, + KnownInstanceType::Annotated(ty) => { + Type::KnownInstance(KnownInstanceType::Annotated( + InternedType::new( + db, + ty.inner(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor), + ) + )) + }, + KnownInstanceType::Callable(callable_type) => { + Type::KnownInstance(KnownInstanceType::Callable( + callable_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + )) + }, + KnownInstanceType::TypeGenericAlias(ty) => { + Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + InternedType::new( + db, + ty.inner(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor), + ) + )) + }, + + KnownInstanceType::SubscriptedProtocol(_) | + KnownInstanceType::SubscriptedGeneric(_) | + KnownInstanceType::TypeAliasType(_) | + KnownInstanceType::Deprecated(_) | + KnownInstanceType::Field(_) | + KnownInstanceType::ConstraintSet(_) | + KnownInstanceType::GenericContext(_) | + KnownInstanceType::Specialization(_) | + KnownInstanceType::Literal(_) | + KnownInstanceType::LiteralStringAlias(_) | + KnownInstanceType::NewType(_) => { + // TODO: ? + self + }, } Type::FunctionLiteral(function) => { @@ -7755,8 +7808,7 @@ impl<'db> Type<'db> { // some other generic context's specialization is applied to it. | Type::ClassLiteral(_) | Type::BoundSuper(_) - | Type::SpecialForm(_) - | Type::KnownInstance(_) => self, + | Type::SpecialForm(_) => self, } } @@ -7893,6 +7945,44 @@ impl<'db> Type<'db> { }); } + Type::KnownInstance(known_instance) => match known_instance { + KnownInstanceType::UnionType(instance) => { + if let Ok(union_type) = instance.union_type(db) { + union_type.find_legacy_typevars_impl( + db, + binding_context, + typevars, + visitor, + ); + } + } + KnownInstanceType::Annotated(ty) => { + ty.inner(db) + .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + } + KnownInstanceType::Callable(callable_type) => { + callable_type.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + } + KnownInstanceType::TypeGenericAlias(ty) => { + ty.inner(db) + .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + } + KnownInstanceType::SubscriptedProtocol(_) + | KnownInstanceType::SubscriptedGeneric(_) + | KnownInstanceType::TypeVar(_) + | KnownInstanceType::TypeAliasType(_) + | KnownInstanceType::Deprecated(_) + | KnownInstanceType::Field(_) + | KnownInstanceType::ConstraintSet(_) + | KnownInstanceType::GenericContext(_) + | KnownInstanceType::Specialization(_) + | KnownInstanceType::Literal(_) + | KnownInstanceType::LiteralStringAlias(_) + | KnownInstanceType::NewType(_) => { + // TODO? + } + }, + Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy @@ -7920,7 +8010,6 @@ impl<'db> Type<'db> { | Type::EnumLiteral(_) | Type::BoundSuper(_) | Type::SpecialForm(_) - | Type::KnownInstance(_) | Type::TypedDict(_) => {} } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 71d4dfec1789b..2aeb7caf91edd 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10860,6 +10860,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); + + if value_ty.is_generic_alias() { + return self + .infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, false); + } + self.infer_subscript_load_impl(value_ty, 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 af23bc646e85b..e947b185d22de 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 @@ -2,6 +2,7 @@ use itertools::Either; use ruff_python_ast as ast; use super::{DeferredExpressionState, TypeInferenceBuilder}; +use crate::FxOrderSet; use crate::types::diagnostic::{ self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, report_invalid_arguments_to_callable, @@ -12,9 +13,9 @@ use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::visitor::any_over_type; use crate::types::{ - CallableType, DynamicType, IntersectionBuilder, KnownClass, KnownInstanceType, - LintDiagnosticGuard, SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeContext, - TypeIsType, UnionBuilder, UnionType, todo_type, + BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, KnownClass, + KnownInstanceType, LintDiagnosticGuard, SpecialFormType, SubclassOfType, Type, TypeAliasType, + TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, todo_type, }; /// Type expressions @@ -734,6 +735,49 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } + pub(crate) fn infer_explicitly_specialized_implicit_type_alias( + &mut self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + in_type_expression: bool, + ) -> Type<'db> { + let db = self.db(); + + let generic_type_alias = value_ty.apply_type_mapping( + db, + &TypeMapping::BindLegacyTypevars(BindingContext::Synthetic), + TypeContext::default(), + ); + + let mut variables = FxOrderSet::default(); + generic_type_alias.find_legacy_typevars(db, None, &mut variables); + let generic_context = GenericContext::from_typevar_instances(db, variables); + + let scope_id = self.scope(); + let typevar_binding_context = self.typevar_binding_context; + let specialize = |types: &[Option>]| { + let specialized = generic_type_alias.apply_specialization( + db, + generic_context.specialize_partial(db, types.iter().copied()), + ); + + if in_type_expression { + specialized + .in_type_expression(db, scope_id, typevar_binding_context) + .unwrap_or_else(|_| Type::unknown()) + } else { + specialized + } + }; + + self.infer_explicit_callable_specialization( + subscript, + value_ty, + generic_context, + specialize, + ) + } + fn infer_subscript_type_expression( &mut self, subscript: &ast::ExprSubscript, @@ -824,10 +868,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - KnownInstanceType::TypeVar(_) => { - self.infer_type_expression(slice); - todo_type!("TypeVar annotations") - } KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => { match type_alias.generic_context(self.db()) { Some(generic_context) => { @@ -870,11 +910,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { 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") - } - KnownInstanceType::Literal(ty) | KnownInstanceType::TypeGenericAlias(ty) => { + KnownInstanceType::Literal(ty) => { self.infer_type_expression(slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( @@ -884,13 +920,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - KnownInstanceType::Callable(_) => { - self.infer_type_expression(slice); - todo_type!("Generic specialization of typing.Callable") - } - KnownInstanceType::Annotated(_) => { - self.infer_type_expression(slice); - todo_type!("Generic specialization of typing.Annotated") + KnownInstanceType::TypeVar(_) => self + .infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, false), + + KnownInstanceType::UnionType(_) + | KnownInstanceType::Callable(_) + | KnownInstanceType::Annotated(_) + | KnownInstanceType::TypeGenericAlias(_) => { + self.infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, true) } KnownInstanceType::NewType(newtype) => { self.infer_type_expression(&subscript.slice); @@ -933,11 +970,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } Type::GenericAlias(_) => { - self.infer_type_expression(slice); - // If the generic alias is already fully specialized, this is an error. But it - // could have been specialized with another typevar (e.g. a type alias like `MyList - // = list[T]`), in which case it's later valid to do `MyList[int]`. - todo_type!("specialized generic alias in type expression") + self.infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, true) } Type::StringLiteral(_) => { self.infer_type_expression(slice); From c770b2a8536d11e459961792b502d2b56705f31d Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 24 Nov 2025 11:27:40 +0100 Subject: [PATCH 02/25] Use assignment definition as typevar binding context --- .../resources/mdtest/implicit_type_aliases.md | 47 +++++++++++++------ .../src/semantic_index/definition.rs | 6 +++ .../src/types/infer/builder.rs | 8 ++++ .../types/infer/builder/type_expression.rs | 27 ++++++++++- 4 files changed, 71 insertions(+), 17 deletions(-) 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 6a3a9ad37d5e4..2c51eacdf500c 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -191,13 +191,13 @@ def _( reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes) reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int # TODO should be Unknown | int - reveal_type(type_var_or_int) # revealed: typing.TypeVar | int + reveal_type(type_var_or_int) # revealed: T@TypeVarOrInt | int # TODO should be int | Unknown - reveal_type(int_or_type_var) # revealed: int | typing.TypeVar + reveal_type(int_or_type_var) # revealed: int | T@IntOrTypeVar # TODO should be Unknown | None - reveal_type(type_var_or_none) # revealed: typing.TypeVar | None + reveal_type(type_var_or_none) # revealed: T@TypeVarOrNone | None # TODO should be None | Unknown - reveal_type(none_or_type_var) # revealed: None | typing.TypeVar + reveal_type(none_or_type_var) # revealed: None | T@NoneOrTypeVar ``` If a type is unioned with itself in a value expression, the result is just that type. No @@ -391,17 +391,17 @@ AnnotatedType = Annotated[T, "tag"] TransparentAlias = T MyOptional = T | None -# TODO: Consider displaying this as ``, … instead? (and similar for some others below) -reveal_type(MyList) # revealed: -reveal_type(MyDict) # revealed: +reveal_type(MyList) # revealed: +reveal_type(MyDict) # revealed: reveal_type(MyType) # revealed: GenericAlias -reveal_type(IntAndType) # revealed: -reveal_type(Pair) # revealed: -reveal_type(Sum) # revealed: +reveal_type(IntAndType) # revealed: +reveal_type(Pair) # revealed: +reveal_type(Sum) # revealed: reveal_type(ListOrTuple) # revealed: types.UnionType reveal_type(ListOrTupleLegacy) # revealed: types.UnionType reveal_type(MyCallable) # revealed: GenericAlias reveal_type(AnnotatedType) # revealed: +# TODO: This should ideally be `T@TransparentAlias` reveal_type(TransparentAlias) # revealed: typing.TypeVar reveal_type(MyOptional) # revealed: types.UnionType @@ -445,7 +445,7 @@ U = TypeVar("U") DictStrTo = MyDict[str, U] -reveal_type(DictStrTo) # revealed: +reveal_type(DictStrTo) # revealed: def _( dict_str_to_int: DictStrTo[int], @@ -480,7 +480,7 @@ A generic implicit type alias can also be used in another generic implicit type ```py MyOtherList = MyList[T] -reveal_type(MyOtherList) # revealed: +reveal_type(MyOtherList) # revealed: def _( list_of_ints: MyOtherList[int], @@ -498,11 +498,11 @@ def _( my_callable: MyCallable, ): # TODO: Should be `list[Unknown]` - reveal_type(my_list) # revealed: list[typing.TypeVar] + reveal_type(my_list) # revealed: list[T@MyList] # TODO: Should be `dict[Unknown, Unknown]` - reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar] + reveal_type(my_dict) # revealed: dict[T@MyDict, U@MyDict] # TODO: Should be `(...) -> Unknown` - reveal_type(my_callable) # revealed: (...) -> typing.TypeVar + reveal_type(my_callable) # revealed: (...) -> T@MyCallable ``` (Generic) implicit type aliases can be used as base classes: @@ -552,6 +552,23 @@ def _( reveal_type(dict_too_few_args) # revealed: Unknown ``` +Trying to specialize a non-name node results in an error: + +```py +from ty_extensions import TypeOf + +IntOrStr = int | str + +def this_does_not_work() -> TypeOf[IntOrStr]: + raise NotImplementedError() + +def _( + # error: [invalid-type-form] "Cannot specialize a non-name node in a type expression" + specialized: this_does_not_work()[int], +): + reveal_type(specialized) # revealed: Unknown +``` + ## `Literal`s We also support `typing.Literal` in implicit type aliases. diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 03acaa53357f9..2659e75493b0c 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -90,6 +90,12 @@ impl<'db> Definition<'db> { .to_string(), ) } + DefinitionKind::Assignment(assignment) => { + let target_node = assignment.target.node(&module); + target_node + .as_name_expr() + .map(|name_expr| name_expr.id.as_str().to_string()) + } _ => None, } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 2aeb7caf91edd..c6b44906eb631 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4796,6 +4796,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { unpacked.expression_type(target) } TargetKind::Single => { + // This could be an implicit type alias (OptionalList = list[T] | None). Use the definition + // of `OptionalList` as the typevar binding context while inferring the RHS (`list[T] | None`), + // in order to bind `T@OptionalList`. + let previous_typevar_binding_context = + self.typevar_binding_context.replace(definition); + let value_ty = if let Some(standalone_expression) = self.index.try_expression(value) { self.infer_standalone_expression_impl(value, standalone_expression, tcx) @@ -4834,6 +4840,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_expression(value, tcx) }; + self.typevar_binding_context = previous_typevar_binding_context; + // `TYPE_CHECKING` is a special variable that should only be assigned `False` // at runtime, but is always considered `True` in type checking. // See mdtest/known_constants.md#user-defined-type_checking for details. 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 e947b185d22de..21217b512ed7c 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 @@ -2,7 +2,6 @@ use itertools::Either; use ruff_python_ast as ast; use super::{DeferredExpressionState, TypeInferenceBuilder}; -use crate::FxOrderSet; use crate::types::diagnostic::{ self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, report_invalid_arguments_to_callable, @@ -17,6 +16,7 @@ use crate::types::{ KnownInstanceType, LintDiagnosticGuard, SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, todo_type, }; +use crate::{FxOrderSet, ResolvedDefinition, definitions_for_name}; /// Type expressions impl<'db> TypeInferenceBuilder<'db, '_> { @@ -743,9 +743,32 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) -> Type<'db> { let db = self.db(); + let Some(value) = subscript.value.as_name_expr() else { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic("Cannot specialize a non-name node in a type expression"); + } + return Type::unknown(); + }; + + // TODO: This is an expensive call to an API that was never meant to be called from + // type inference. We plan to rework how `in_type_expression` works in the future. + // This new approach will make this call unnecessary, so for now, we accept the hit + // in performance. + let definitions = definitions_for_name(self.db(), self.file(), value); + let Some(type_alias_definition) = + definitions.iter().find_map(ResolvedDefinition::definition) + else { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic( + "Cannot specialize implicit type alias with unknown definition", + ); + } + return Type::unknown(); + }; + let generic_type_alias = value_ty.apply_type_mapping( db, - &TypeMapping::BindLegacyTypevars(BindingContext::Synthetic), + &TypeMapping::BindLegacyTypevars(BindingContext::Definition(type_alias_definition)), TypeContext::default(), ); From 4fb090948e03553119b8b0b4a8fa72d3ed5bc009 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 24 Nov 2025 12:12:27 +0100 Subject: [PATCH 03/25] Handle attribute expressions as well --- .../resources/mdtest/implicit_type_aliases.md | 111 +++++++++++++++++- .../types/infer/builder/type_expression.rs | 36 ++++-- 2 files changed, 135 insertions(+), 12 deletions(-) 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 2c51eacdf500c..7f0cab333fcaa 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -366,7 +366,9 @@ def g(obj: Y): reveal_type(obj) # revealed: list[int | str] ``` -## Generic types +## Generic implicit type aliases + +### Functionality Implicit type aliases can also be generic: @@ -528,6 +530,36 @@ class Derived2(GenericBaseAlias[int]): pass ``` +### Imported aliases + +Generic implicit type aliases can be imported from other modules and specialized: + +`my_types.py`: + +```py +from typing_extensions import TypeVar + +T = TypeVar("T") + +MyList = list[T] +``` + +`main.py`: + +```py +from my_types import MyList +import my_types as mt + +def _( + list_of_ints1: MyList[int], + list_of_ints2: mt.MyList[int], +): + reveal_type(list_of_ints1) # revealed: list[int] + reveal_type(list_of_ints2) # revealed: list[int] +``` + +### Error cases + A generic alias that is already fully specialized cannot be specialized again: ```py @@ -542,6 +574,14 @@ Specializing a generic implicit type alias with an incorrect number of type argu in an error: ```py +from typing_extensions import TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +MyList = list[T] +MyDict = dict[T, U] + def _( # error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2" list_too_many_args: MyList[int, str], @@ -563,12 +603,79 @@ def this_does_not_work() -> TypeOf[IntOrStr]: raise NotImplementedError() def _( - # error: [invalid-type-form] "Cannot specialize a non-name node in a type expression" + # error: [invalid-type-form] "Only name- and attribute expressions can be specialized in type expressions" specialized: this_does_not_work()[int], ): reveal_type(specialized) # revealed: Unknown ``` +### Multiple definitions + +#### Shadowed definitions + +When a generic type alias shadows a definition from an outer scope, the inner definition is used: + +```py +from typing_extensions import TypeVar + +T = TypeVar("T") + +MyAlias = list[T] + +def outer(): + MyAlias = set[T] + + def _(x: MyAlias[int]): + reveal_type(x) # revealed: set[int] +``` + +#### Statically known conditions + +```py +from typing_extensions import TypeVar + +T = TypeVar("T") + +if True: + MyAlias1 = list[T] +else: + MyAlias1 = set[T] + +if False: + MyAlias2 = list[T] +else: + MyAlias2 = set[T] + +def _( + x1: MyAlias1[int], + x2: MyAlias2[int], +): + reveal_type(x1) # revealed: list[int] + reveal_type(x2) # revealed: set[int] +``` + +#### Statically unknown conditions + +If several definitions are visible, we emit an error: + +```py +from typing_extensions import TypeVar + +T = TypeVar("T") + +def flag() -> bool: + return True + +if flag(): + MyAlias = list[T] +else: + MyAlias = set[T] + +# error: [invalid-type-form] "Invalid subscript of object of type ` | ` in type expression" +def _(x: MyAlias[int]): + reveal_type(x) # revealed: Unknown +``` + ## `Literal`s We also support `typing.Literal` in implicit type aliases. 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 21217b512ed7c..cf18db389c345 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 @@ -16,7 +16,7 @@ use crate::types::{ KnownInstanceType, LintDiagnosticGuard, SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, todo_type, }; -use crate::{FxOrderSet, ResolvedDefinition, definitions_for_name}; +use crate::{FxOrderSet, ResolvedDefinition, definitions_for_attribute, definitions_for_name}; /// Type expressions impl<'db> TypeInferenceBuilder<'db, '_> { @@ -743,18 +743,34 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) -> Type<'db> { let db = self.db(); - let Some(value) = subscript.value.as_name_expr() else { - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - builder.into_diagnostic("Cannot specialize a non-name node in a type expression"); + let definitions = match &*subscript.value { + ast::Expr::Name(id) => { + // TODO: This is an expensive call to an API that was never meant to be called from + // type inference. We plan to rework how `in_type_expression` works in the future. + // This new approach will make this call unnecessary, so for now, we accept the hit + // in performance. + definitions_for_name(self.db(), self.file(), id) + } + ast::Expr::Attribute(attribute) => { + // TODO: See above + definitions_for_attribute(self.db(), self.file(), attribute) + } + _ => { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic( + "Only name- and attribute expressions can be specialized in type expressions", + ); + } + return Type::unknown(); } - return Type::unknown(); }; - // TODO: This is an expensive call to an API that was never meant to be called from - // type inference. We plan to rework how `in_type_expression` works in the future. - // This new approach will make this call unnecessary, so for now, we accept the hit - // in performance. - let definitions = definitions_for_name(self.db(), self.file(), value); + // TODO: If an implicit type alias is defined multiple times, we arbitrarily pick the + // first definition here. Instead, we should do proper name resolution to find the + // definition that is actually being referenced. Similar to the comments above, this + // should soon be addressed by a rework of how `in_type_expression` works. In the + // meantime, we seem to be doing okay in practice (see "Multiple definitions" tests in + // `implicit_type_aliases.md`). let Some(type_alias_definition) = definitions.iter().find_map(ResolvedDefinition::definition) else { From 24d36936a4fbb3f67d4412ab3b4533fd8546a9fb Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 24 Nov 2025 13:15:11 +0100 Subject: [PATCH 04/25] Handle PEP 613 aliases as well --- .../resources/mdtest/pep613_type_aliases.md | 18 ++++++++++++++++++ .../src/types/infer/builder.rs | 5 +++++ 2 files changed, 23 insertions(+) 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 40e5d2c47723a..768eef0499086 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -96,6 +96,24 @@ def _(x: MyAlias): reveal_type(x) # revealed: int | ((str, /) -> int) ``` +## Generic aliases + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T") + +MyList: TypeAlias = list[T] +ListOrSet: TypeAlias = list[T] | set[T] + +reveal_type(MyList) # revealed: +reveal_type(ListOrSet) # revealed: types.UnionType + +def _(list_of_int: MyList[int], list_or_set_of_str: ListOrSet[str]): + reveal_type(list_of_int) # revealed: list[int] + reveal_type(list_or_set_of_str) # revealed: list[str] | set[str] +``` + ## Subscripted generic alias in union ```py diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index c6b44906eb631..f68cb17b0a5ce 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5588,11 +5588,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.deferred_state = DeferredExpressionState::Deferred; } + let previous_typevar_binding_context = self.typevar_binding_context; + self.typevar_binding_context = Some(definition); + let inferred_ty = self.infer_maybe_standalone_expression( value, TypeContext::new(Some(declared.inner_type())), ); + self.typevar_binding_context = previous_typevar_binding_context; + self.deferred_state = previous_deferred_state; self.dataclass_field_specifiers.clear(); From 646c4624119b120a8b8d74666ae7db695035d257 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 24 Nov 2025 13:48:11 +0100 Subject: [PATCH 05/25] Patch panics for stringified annotations for now --- .../resources/mdtest/implicit_type_aliases.md | 17 +++++++++++++++++ .../src/types/infer/builder/type_expression.rs | 8 ++++++++ 2 files changed, 25 insertions(+) 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 7f0cab333fcaa..97bce3663831b 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -558,6 +558,23 @@ def _( reveal_type(list_of_ints2) # revealed: list[int] ``` +### In stringified annotations + +Generic implicit type aliases can be specialized in stringified annotations: + +```py +from typing_extensions import TypeVar + +T = TypeVar("T") + +MyList = list[T] + +def _( + list_of_ints: "MyList[int]", +): + reveal_type(list_of_ints) # revealed: @Todo(Specialization of generic type alias in stringified annotation) +``` + ### Error cases A generic alias that is already fully specialized cannot be specialized again: 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 cf18db389c345..6f90ce70bd728 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 @@ -743,6 +743,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) -> Type<'db> { let db = self.db(); + if self + .index + .try_expression_scope_id(&ast::ExprRef::from(subscript)) + .is_none() + { + return todo_type!("Specialization of generic type alias in stringified annotation"); + } + let definitions = match &*subscript.value { ast::Expr::Name(id) => { // TODO: This is an expensive call to an API that was never meant to be called from From 225edf1a0307f71c768cf808632c21f899f0c7fb Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 24 Nov 2025 16:49:45 +0100 Subject: [PATCH 06/25] Rename --- crates/ty_python_semantic/src/types/infer/builder.rs | 3 +-- .../src/types/infer/builder/type_expression.rs | 12 +++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f68cb17b0a5ce..505046085da8a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10875,8 +10875,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); if value_ty.is_generic_alias() { - return self - .infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, false); + return self.infer_explicitly_specialized_type_alias(subscript, value_ty, false); } self.infer_subscript_load_impl(value_ty, 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 6f90ce70bd728..e077c46f426a3 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 @@ -735,7 +735,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - pub(crate) fn infer_explicitly_specialized_implicit_type_alias( + /// Infer the type of an explicitly specialized generic type alias (implicit or PEP 613). + pub(crate) fn infer_explicitly_specialized_type_alias( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, @@ -967,14 +968,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - KnownInstanceType::TypeVar(_) => self - .infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, false), + KnownInstanceType::TypeVar(_) => { + self.infer_explicitly_specialized_type_alias(subscript, value_ty, false) + } KnownInstanceType::UnionType(_) | KnownInstanceType::Callable(_) | KnownInstanceType::Annotated(_) | KnownInstanceType::TypeGenericAlias(_) => { - self.infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, true) + self.infer_explicitly_specialized_type_alias(subscript, value_ty, true) } KnownInstanceType::NewType(newtype) => { self.infer_type_expression(&subscript.slice); @@ -1017,7 +1019,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } Type::GenericAlias(_) => { - self.infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, true) + self.infer_explicitly_specialized_type_alias(subscript, value_ty, true) } Type::StringLiteral(_) => { self.infer_type_expression(slice); From d6981e8101dbe912413f1cc924c21db6b1dccba2 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 24 Nov 2025 22:00:48 +0100 Subject: [PATCH 07/25] Fix value-position specializations --- .../resources/mdtest/implicit_type_aliases.md | 20 +++++++++++++++++++ .../resources/mdtest/pep613_type_aliases.md | 6 ++++-- .../src/types/infer/builder.rs | 9 +++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) 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 97bce3663831b..77e5aaeb3a1b3 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -462,19 +462,39 @@ expected: IntsOrNone = MyList[int] | None IntsOrStrs = Pair[int] | Pair[str] ListOfPairs = MyList[Pair[str]] +ListOrTupleOfInts = ListOrTuple[int] +AnnotatedInt = AnnotatedType[int] +SubclassOfInt = MyType[int] +# TODO: No error here +# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2" +# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `list[int]`?" +CallableIntToStr = MyCallable[[int], str] reveal_type(IntsOrNone) # revealed: types.UnionType reveal_type(IntsOrStrs) # revealed: types.UnionType reveal_type(ListOfPairs) # revealed: +reveal_type(ListOrTupleOfInts) # revealed: types.UnionType +reveal_type(AnnotatedInt) # revealed: +reveal_type(SubclassOfInt) # revealed: GenericAlias +reveal_type(CallableIntToStr) # revealed: Unknown def _( ints_or_none: IntsOrNone, ints_or_strs: IntsOrStrs, list_of_pairs: ListOfPairs, + list_or_tuple_of_ints: ListOrTupleOfInts, + annotated_int: AnnotatedInt, + subclass_of_int: SubclassOfInt, + callable_int_to_str: CallableIntToStr, ): reveal_type(ints_or_none) # revealed: list[int] | None reveal_type(ints_or_strs) # revealed: tuple[int, int] | tuple[str, str] reveal_type(list_of_pairs) # revealed: list[tuple[str, str]] + reveal_type(list_or_tuple_of_ints) # revealed: list[int] | tuple[int, ...] + reveal_type(annotated_int) # revealed: int + reveal_type(subclass_of_int) # revealed: type[int] + # TODO: This should be `(int, /) -> str` + reveal_type(callable_int_to_str) # revealed: Unknown ``` A generic implicit type alias can also be used in another generic implicit type alias: 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 768eef0499086..59483ce8d7505 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -124,9 +124,11 @@ T = TypeVar("T") Alias1: TypeAlias = list[T] | set[T] MyAlias: TypeAlias = int | Alias1[str] +def _(): + reveal_type(Alias1) # revealed: types.UnionType + def _(x: MyAlias): - # TODO: int | list[str] | set[str] - reveal_type(x) # revealed: int | @Todo(Specialization of union type alias) + reveal_type(x) # revealed: int | list[str] | set[str] ``` ## Imported diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 505046085da8a..c9ecf266d0b30 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -11179,8 +11179,13 @@ 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"); + Type::KnownInstance( + KnownInstanceType::UnionType(_) + | KnownInstanceType::Annotated(_) + | KnownInstanceType::Callable(_) + | KnownInstanceType::TypeGenericAlias(_), + ) => { + return self.infer_explicitly_specialized_type_alias(subscript, value_ty, false); } _ => {} } From 0968622e83b6dce885fcac05a5565238f4166598 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 25 Nov 2025 11:13:22 +0100 Subject: [PATCH 08/25] Fix problem with np.array related to type[T] --- .../resources/mdtest/implicit_type_aliases.md | 24 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 10 ++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) 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 77e5aaeb3a1b3..1c96ea7173650 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -500,14 +500,27 @@ def _( A generic implicit type alias can also be used in another generic implicit type alias: ```py +from typing_extensions import Any + +B = TypeVar("B", bound=int) + MyOtherList = MyList[T] +MyOtherType = MyType[T] +TypeOrList = MyType[B] | MyList[B] reveal_type(MyOtherList) # revealed: +reveal_type(MyOtherType) # revealed: GenericAlias +reveal_type(TypeOrList) # revealed: types.UnionType def _( list_of_ints: MyOtherList[int], + subclass_of_int: MyOtherType[int], + type_or_list: TypeOrList[Any], ): reveal_type(list_of_ints) # revealed: list[int] + reveal_type(subclass_of_int) # revealed: type[int] + # TODO: Should be `type[Any] | list[Any]` + reveal_type(type_or_list) # revealed: @Todo(type[T] for typevar T) | list[Any] ``` If a generic implicit type alias is used unspecialized in a type expression, we treat it as an @@ -1455,3 +1468,14 @@ def _( reveal_type(recursive_dict3) # revealed: dict[Divergent, int] reveal_type(recursive_dict4) # revealed: dict[Divergent, int] ``` + +### Self-referential generic implicit type aliases + + + +```py +from typing import TypeVar + +T = TypeVar("T") +NestedDict = dict[str, "NestedDict[T] | T"] +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index c42d28ef9df35..0e7834a25ac4f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7219,7 +7219,7 @@ impl<'db> Type<'db> { } KnownInstanceType::Literal(ty) => Ok(ty.inner(db)), KnownInstanceType::Annotated(ty) => Ok(ty.inner(db)), - KnownInstanceType::TypeGenericAlias(ty) => { + KnownInstanceType::TypeGenericAlias(instance) => { // When `type[…]` appears in a value position (e.g. in an implicit type alias), // we infer its argument as a type expression. This ensures that we can emit // diagnostics for invalid type expressions, and more importantly, that we can @@ -7228,7 +7228,13 @@ impl<'db> Type<'db> { // (`int` -> instance of `int` -> subclass of `int`) can be lossy, but it is // okay for all valid arguments to `type[…]`. - Ok(ty.inner(db).to_meta_type(db)) + let ty = instance.inner(db); + + if ty.is_type_var() { + Ok(todo_type!("type[T] for typevar T")) + } else { + Ok(ty.to_meta_type(db)) + } } KnownInstanceType::Callable(callable) => Ok(Type::Callable(*callable)), KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)), From a3ee7e8c396221688c66ce0fd647ddf81111a2a8 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 26 Nov 2025 15:16:06 +0100 Subject: [PATCH 09/25] Stringified annotations --- .../resources/mdtest/implicit_type_aliases.md | 4 +- .../types/infer/builder/type_expression.rs | 63 +++---------------- 2 files changed, 12 insertions(+), 55 deletions(-) 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 1c96ea7173650..ce9ab0ad7068a 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -605,7 +605,7 @@ MyList = list[T] def _( list_of_ints: "MyList[int]", ): - reveal_type(list_of_ints) # revealed: @Todo(Specialization of generic type alias in stringified annotation) + reveal_type(list_of_ints) # revealed: list[int] ``` ### Error cases @@ -653,7 +653,7 @@ def this_does_not_work() -> TypeOf[IntOrStr]: raise NotImplementedError() def _( - # error: [invalid-type-form] "Only name- and attribute expressions can be specialized in type expressions" + # error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 1" specialized: this_does_not_work()[int], ): reveal_type(specialized) # revealed: Unknown 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 e077c46f426a3..671e4d8da77f4 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 @@ -739,72 +739,29 @@ impl<'db> TypeInferenceBuilder<'db, '_> { pub(crate) fn infer_explicitly_specialized_type_alias( &mut self, subscript: &ast::ExprSubscript, - value_ty: Type<'db>, + mut value_ty: Type<'db>, in_type_expression: bool, ) -> Type<'db> { let db = self.db(); - if self - .index - .try_expression_scope_id(&ast::ExprRef::from(subscript)) - .is_none() + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_ty + && let Some(definition) = typevar.definition(db) { - return todo_type!("Specialization of generic type alias in stringified annotation"); + value_ty = value_ty.apply_type_mapping( + db, + &TypeMapping::BindLegacyTypevars(BindingContext::Definition(definition)), + TypeContext::default(), + ); } - let definitions = match &*subscript.value { - ast::Expr::Name(id) => { - // TODO: This is an expensive call to an API that was never meant to be called from - // type inference. We plan to rework how `in_type_expression` works in the future. - // This new approach will make this call unnecessary, so for now, we accept the hit - // in performance. - definitions_for_name(self.db(), self.file(), id) - } - ast::Expr::Attribute(attribute) => { - // TODO: See above - definitions_for_attribute(self.db(), self.file(), attribute) - } - _ => { - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - builder.into_diagnostic( - "Only name- and attribute expressions can be specialized in type expressions", - ); - } - return Type::unknown(); - } - }; - - // TODO: If an implicit type alias is defined multiple times, we arbitrarily pick the - // first definition here. Instead, we should do proper name resolution to find the - // definition that is actually being referenced. Similar to the comments above, this - // should soon be addressed by a rework of how `in_type_expression` works. In the - // meantime, we seem to be doing okay in practice (see "Multiple definitions" tests in - // `implicit_type_aliases.md`). - let Some(type_alias_definition) = - definitions.iter().find_map(ResolvedDefinition::definition) - else { - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - builder.into_diagnostic( - "Cannot specialize implicit type alias with unknown definition", - ); - } - return Type::unknown(); - }; - - let generic_type_alias = value_ty.apply_type_mapping( - db, - &TypeMapping::BindLegacyTypevars(BindingContext::Definition(type_alias_definition)), - TypeContext::default(), - ); - let mut variables = FxOrderSet::default(); - generic_type_alias.find_legacy_typevars(db, None, &mut variables); + value_ty.find_legacy_typevars(db, None, &mut variables); let generic_context = GenericContext::from_typevar_instances(db, variables); let scope_id = self.scope(); let typevar_binding_context = self.typevar_binding_context; let specialize = |types: &[Option>]| { - let specialized = generic_type_alias.apply_specialization( + let specialized = value_ty.apply_specialization( db, generic_context.specialize_partial(db, types.iter().copied()), ); From 272cb4b1eb669e70ded6913548c3dca78ec0957d Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 26 Nov 2025 15:22:39 +0100 Subject: [PATCH 10/25] Cleanup --- .../resources/mdtest/implicit_type_aliases.md | 14 ++++++++++++-- .../resources/mdtest/pep613_type_aliases.md | 3 --- crates/ty_python_semantic/src/types.rs | 12 ++++++++++-- .../ty_python_semantic/src/types/infer/builder.rs | 13 +++++++++---- .../src/types/infer/builder/type_expression.rs | 6 +++--- 5 files changed, 34 insertions(+), 14 deletions(-) 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 ce9ab0ad7068a..0752cd2422f47 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -443,8 +443,6 @@ def _( Generic implicit type aliases can be partially specialized: ```py -U = TypeVar("U") - DictStrTo = MyDict[str, U] reveal_type(DictStrTo) # revealed: @@ -653,12 +651,24 @@ def this_does_not_work() -> TypeOf[IntOrStr]: raise NotImplementedError() def _( + # TODO: Better error message (of kind `invalid-type-form`)? # error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 1" specialized: this_does_not_work()[int], ): reveal_type(specialized) # revealed: Unknown ``` +Similarly, if you try to specialize a union type without a binding context, we emit an error: + +```py +# TODO: Better error message (of kind `invalid-type-form`)? +# error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 1" +x: (list[T] | set[T])[int] + +def _(): + reveal_type(x) # revealed: Unknown +``` + ### Multiple definitions #### Shadowed definitions 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 59483ce8d7505..ee97d408067bc 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -124,9 +124,6 @@ T = TypeVar("T") Alias1: TypeAlias = list[T] | set[T] MyAlias: TypeAlias = int | Alias1[str] -def _(): - reveal_type(Alias1) # revealed: types.UnionType - def _(x: MyAlias): reveal_type(x) # revealed: int | list[str] | set[str] ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 0e7834a25ac4f..52e8fda6ea5c5 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7231,6 +7231,14 @@ impl<'db> Type<'db> { let ty = instance.inner(db); if ty.is_type_var() { + // TODO: + // This is a temporary workaround until we have proper support for type[T]. + // If we pass a typevar to `.to_meta_type()`, we currently get `type[B]`, + // where `B` is the upper bound of `T`. However, we really need `type[T]` + // here. Otherwise, when we specialize a generic implicit type alias like + // `TypeOrList[T] = type[T] | list[T]` using `TypeOrList[Any]`, we would get + // `type[B] | list[Any]`, which leads to a lot of false positives for numpy- + // users. Ok(todo_type!("type[T] for typevar T")) } else { Ok(ty.to_meta_type(db)) @@ -7646,7 +7654,7 @@ impl<'db> Type<'db> { KnownInstanceType::Literal(_) | KnownInstanceType::LiteralStringAlias(_) | KnownInstanceType::NewType(_) => { - // TODO: ? + // TODO: For some of these, we may need to apply the type mapping to inner types. self }, } @@ -7985,7 +7993,7 @@ impl<'db> Type<'db> { | KnownInstanceType::Literal(_) | KnownInstanceType::LiteralStringAlias(_) | KnownInstanceType::NewType(_) => { - // TODO? + // TODO: For some of these, we may need to try to find legacy typevars in inner types. } }, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index c9ecf266d0b30..01ad3fb033a91 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4797,8 +4797,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } TargetKind::Single => { // This could be an implicit type alias (OptionalList = list[T] | None). Use the definition - // of `OptionalList` as the typevar binding context while inferring the RHS (`list[T] | None`), - // in order to bind `T@OptionalList`. + // 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); @@ -5588,8 +5588,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.deferred_state = DeferredExpressionState::Deferred; } - let previous_typevar_binding_context = self.typevar_binding_context; - self.typevar_binding_context = Some(definition); + // This might be a PEP-613 type alias (`OptionalList: TypeAlias = list[T] | None`). Use + // 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 inferred_ty = self.infer_maybe_standalone_expression( value, @@ -10874,6 +10876,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); + // If we have an implicit type alias like `MyList = list[T]`, and if `MyList` is being + // used in another implicit type alias like `Numbers = MyList[int]`, then we infer the + // right hand side as a value expression, and need to handle the specialization here. if value_ty.is_generic_alias() { return self.infer_explicitly_specialized_type_alias(subscript, value_ty, false); } 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 671e4d8da77f4..84ac8e7d0d1c6 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 @@ -2,6 +2,7 @@ use itertools::Either; use ruff_python_ast as ast; use super::{DeferredExpressionState, TypeInferenceBuilder}; +use crate::FxOrderSet; use crate::types::diagnostic::{ self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, report_invalid_arguments_to_callable, @@ -16,7 +17,6 @@ use crate::types::{ KnownInstanceType, LintDiagnosticGuard, SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, todo_type, }; -use crate::{FxOrderSet, ResolvedDefinition, definitions_for_attribute, definitions_for_name}; /// Type expressions impl<'db> TypeInferenceBuilder<'db, '_> { @@ -759,7 +759,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let generic_context = GenericContext::from_typevar_instances(db, variables); let scope_id = self.scope(); - let typevar_binding_context = self.typevar_binding_context; + let current_typevar_binding_context = self.typevar_binding_context; let specialize = |types: &[Option>]| { let specialized = value_ty.apply_specialization( db, @@ -768,7 +768,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if in_type_expression { specialized - .in_type_expression(db, scope_id, typevar_binding_context) + .in_type_expression(db, scope_id, current_typevar_binding_context) .unwrap_or_else(|_| Type::unknown()) } else { specialized From a490411610e71508aef35a574e3c76cba8f6b168 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 27 Nov 2025 09:21:07 +0100 Subject: [PATCH 11/25] Adapt tests --- .../resources/mdtest/async.md | 3 +- .../resources/mdtest/implicit_type_aliases.md | 32 ++++++++++--------- .../resources/mdtest/pep613_type_aliases.md | 15 +++++---- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/async.md b/crates/ty_python_semantic/resources/mdtest/async.md index c46d0f86ba8a6..416c88b09c733 100644 --- a/crates/ty_python_semantic/resources/mdtest/async.md +++ b/crates/ty_python_semantic/resources/mdtest/async.md @@ -61,8 +61,7 @@ async def main(): result = await task - # TODO: this should be `int` - reveal_type(result) # revealed: Unknown + reveal_type(result) # revealed: int ``` ### `asyncio.gather` 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 0752cd2422f47..42045a744227c 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -418,7 +418,7 @@ def _( list_or_tuple_legacy: ListOrTupleLegacy[int], # TODO: no errors here # error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[str, bytes]`?" - # error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2" + # error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" my_callable: MyCallable[[str, bytes], int], annotated_int: AnnotatedType[int], transparent_alias: TransparentAlias[int], @@ -464,7 +464,7 @@ ListOrTupleOfInts = ListOrTuple[int] AnnotatedInt = AnnotatedType[int] SubclassOfInt = MyType[int] # TODO: No error here -# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2" +# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" # error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `list[int]`?" CallableIntToStr = MyCallable[[int], str] @@ -613,7 +613,7 @@ A generic alias that is already fully specialized cannot be specialized again: ```py ListOfInts = list[int] -# error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 1" +# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" def _(doubly_specialized: ListOfInts[int]): reveal_type(doubly_specialized) # revealed: Unknown ``` @@ -631,9 +631,9 @@ MyList = list[T] MyDict = dict[T, U] def _( - # error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2" + # error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" list_too_many_args: MyList[int, str], - # error: [missing-argument] "No argument provided for required parameter `U`" + # error: [invalid-type-arguments] "No type argument provided for required type variable `U`" dict_too_few_args: MyDict[int], ): reveal_type(list_too_many_args) # revealed: Unknown @@ -652,7 +652,7 @@ def this_does_not_work() -> TypeOf[IntOrStr]: def _( # TODO: Better error message (of kind `invalid-type-form`)? - # error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 1" + # error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" specialized: this_does_not_work()[int], ): reveal_type(specialized) # revealed: Unknown @@ -662,7 +662,7 @@ Similarly, if you try to specialize a union type without a binding context, we e ```py # TODO: Better error message (of kind `invalid-type-form`)? -# error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 1" +# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" x: (list[T] | set[T])[int] def _(): @@ -731,9 +731,10 @@ if flag(): else: MyAlias = set[T] -# error: [invalid-type-form] "Invalid subscript of object of type ` | ` in type expression" +# TODO: This should be an error like "Invalid subscript of object of type ` | ` in type expression" def _(x: MyAlias[int]): - reveal_type(x) # revealed: Unknown + # TODO: This should be `Unknown` + reveal_type(x) # revealed: list[int] | set[int] ``` ## `Literal`s @@ -1481,11 +1482,12 @@ def _( ### Self-referential generic implicit type aliases - - ```py -from typing import TypeVar - -T = TypeVar("T") -NestedDict = dict[str, "NestedDict[T] | T"] +# TODO: uncomment these +# from typing import TypeVar +# +# T = TypeVar("T") +# NestedDict = dict[str, "NestedDict[T] | T"] +# +# NestedList = list["NestedList[T] | 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 ee97d408067bc..787dfeb05c80b 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -184,13 +184,14 @@ def my_isinstance(obj: object, classinfo: ClassInfo) -> bool: reveal_type(classinfo) # revealed: type | UnionType | tuple[Divergent, ...] return isinstance(obj, classinfo) -K = TypeVar("K") -V = TypeVar("V") -NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]] - -def _(nested: NestedDict[str, int]): - # TODO should be `dict[str, int | NestedDict[str, int]]` - reveal_type(nested) # revealed: @Todo(specialized generic alias in type expression) +# TODO: uncomment this block +# K = TypeVar("K") +# V = TypeVar("V") +# NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]] + +# def _(nested: NestedDict[str, int]): +# # TODO should be `dict[str, int | NestedDict[str, int]]` +# reveal_type(nested) # revealed: @Todo(specialized generic alias in type expression) my_isinstance(1, int) my_isinstance(1, int | str) From 7b6ef8a95353c9b90d7806e19fdc26bbb17cda31 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 27 Nov 2025 10:51:57 +0100 Subject: [PATCH 12/25] Add tests for default specialization --- .../resources/mdtest/implicit_type_aliases.md | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) 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 42045a744227c..d80d14cc347cd 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -403,7 +403,6 @@ reveal_type(ListOrTuple) # revealed: types.UnionType reveal_type(ListOrTupleLegacy) # revealed: types.UnionType reveal_type(MyCallable) # revealed: GenericAlias reveal_type(AnnotatedType) # revealed: -# TODO: This should ideally be `T@TransparentAlias` reveal_type(TransparentAlias) # revealed: typing.TypeVar reveal_type(MyOptional) # revealed: types.UnionType @@ -432,7 +431,6 @@ def _( reveal_type(int_and_bytes) # revealed: tuple[int, bytes] reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...] reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...] - reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...] # TODO: This should be `(str, bytes) -> int` reveal_type(my_callable) # revealed: Unknown reveal_type(annotated_int) # revealed: int @@ -521,21 +519,61 @@ def _( reveal_type(type_or_list) # revealed: @Todo(type[T] for typevar T) | list[Any] ``` -If a generic implicit type alias is used unspecialized in a type expression, we treat it as an -`Unknown` specialization: +If a generic implicit type alias is used unspecialized in a type expression, we use the default +specialization. For type variables without defaults, this is `Unknown`: ```py def _( - my_list: MyList, - my_dict: MyDict, + list_unknown: MyList, + dict_unknown: MyDict, + subclass_of_unknown: MyType, + int_and_unknown: IntAndType, + pair_of_unknown: Pair, + unknown_and_unknown: Sum, + list_or_tuple: ListOrTuple, + list_or_tuple_legacy: ListOrTupleLegacy, my_callable: MyCallable, + annotated_unknown: AnnotatedType, + optional_unknown: MyOptional, ): - # TODO: Should be `list[Unknown]` - reveal_type(my_list) # revealed: list[T@MyList] - # TODO: Should be `dict[Unknown, Unknown]` - reveal_type(my_dict) # revealed: dict[T@MyDict, U@MyDict] + # TODO: This should be `list[Unknown]` + reveal_type(list_unknown) # revealed: list[T@MyList] + # TODO: This should be `dict[Unknown, Unknown]` + reveal_type(dict_unknown) # revealed: dict[T@MyDict, U@MyDict] + # TODO: Should be `type[Unknown]` + reveal_type(subclass_of_unknown) # revealed: @Todo(type[T] for typevar T) + # TODO: Should be `tuple[int, Unknown]` + reveal_type(int_and_unknown) # revealed: tuple[int, T@IntAndType] + # TODO: Should be `tuple[Unknown, Unknown]` + reveal_type(pair_of_unknown) # revealed: tuple[T@Pair, T@Pair] + # TODO: Should be `tuple[Unknown, Unknown]` + reveal_type(unknown_and_unknown) # revealed: tuple[T@Sum, U@Sum] + # TODO: Should be `list[Unknown] | tuple[Unknown, ...]` + reveal_type(list_or_tuple) # revealed: list[T@ListOrTuple] | tuple[T@ListOrTuple, ...] + # TODO: Should be `list[Unknown] | tuple[Unknown, ...]` + reveal_type(list_or_tuple_legacy) # revealed: list[T@ListOrTupleLegacy] | tuple[T@ListOrTupleLegacy, ...] # TODO: Should be `(...) -> Unknown` reveal_type(my_callable) # revealed: (...) -> T@MyCallable + # TODO: Should be `Unknown` + reveal_type(annotated_unknown) # revealed: T@AnnotatedType + # TODO: Should be `Unknown | None` + reveal_type(optional_unknown) # revealed: T@MyOptional | None +``` + +For a type variable with a default, we use that: + +```py +T_default = TypeVar("T_default", default=int) + +MyListWithDefault = list[T_default] + +def _( + list_of_str: MyListWithDefault[str], + list_of_int: MyListWithDefault, +): + reveal_type(list_of_str) # revealed: list[str] + # TODO: this should be `list[int]` + reveal_type(list_of_int) # revealed: list[T_default@MyListWithDefault] ``` (Generic) implicit type aliases can be used as base classes: From 801c52b3b02e3b86eb1e1ba03ff5b1e66230ad77 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 27 Nov 2025 11:16:57 +0100 Subject: [PATCH 13/25] Rename methods --- crates/ty_python_semantic/src/types/infer/builder.rs | 8 ++++---- .../src/types/infer/builder/type_expression.rs | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 01ad3fb033a91..fd8cdd168e2f0 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10880,7 +10880,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // used in another implicit type alias like `Numbers = MyList[int]`, then we infer the // right hand side as a value expression, and need to handle the specialization here. if value_ty.is_generic_alias() { - return self.infer_explicitly_specialized_type_alias(subscript, value_ty, false); + return self.infer_explicit_type_alias_specialization(subscript, value_ty, false); } self.infer_subscript_load_impl(value_ty, subscript) @@ -10952,7 +10952,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { if let Some(generic_context) = type_alias.generic_context(self.db()) { - return self.infer_explicit_type_alias_specialization( + return self.infer_explicit_type_alias_type_specialization( subscript, value_ty, type_alias, @@ -11190,7 +11190,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | KnownInstanceType::Callable(_) | KnownInstanceType::TypeGenericAlias(_), ) => { - return self.infer_explicitly_specialized_type_alias(subscript, value_ty, false); + return self.infer_explicit_type_alias_specialization(subscript, value_ty, false); } _ => {} } @@ -11222,7 +11222,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) } - fn infer_explicit_type_alias_specialization( + fn infer_explicit_type_alias_type_specialization( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, 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 84ac8e7d0d1c6..80882cbaf9f98 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 @@ -736,7 +736,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } /// Infer the type of an explicitly specialized generic type alias (implicit or PEP 613). - pub(crate) fn infer_explicitly_specialized_type_alias( + pub(crate) fn infer_explicit_type_alias_specialization( &mut self, subscript: &ast::ExprSubscript, mut value_ty: Type<'db>, @@ -877,7 +877,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { match type_alias.generic_context(self.db()) { Some(generic_context) => { let specialized_type_alias = self - .infer_explicit_type_alias_specialization( + .infer_explicit_type_alias_type_specialization( subscript, value_ty, type_alias, @@ -926,14 +926,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::unknown() } KnownInstanceType::TypeVar(_) => { - self.infer_explicitly_specialized_type_alias(subscript, value_ty, false) + self.infer_explicit_type_alias_specialization(subscript, value_ty, false) } KnownInstanceType::UnionType(_) | KnownInstanceType::Callable(_) | KnownInstanceType::Annotated(_) | KnownInstanceType::TypeGenericAlias(_) => { - self.infer_explicitly_specialized_type_alias(subscript, value_ty, true) + self.infer_explicit_type_alias_specialization(subscript, value_ty, true) } KnownInstanceType::NewType(newtype) => { self.infer_type_expression(&subscript.slice); @@ -976,7 +976,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } Type::GenericAlias(_) => { - self.infer_explicitly_specialized_type_alias(subscript, value_ty, true) + self.infer_explicit_type_alias_specialization(subscript, value_ty, true) } Type::StringLiteral(_) => { self.infer_type_expression(slice); From e13e49c8ff1036b450778038b87a7976a212c41a Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 27 Nov 2025 13:53:01 +0100 Subject: [PATCH 14/25] Fix tests after rebase --- .../resources/mdtest/implicit_type_aliases.md | 42 ++++++++++++------- .../resources/mdtest/pep613_type_aliases.md | 19 +++++---- 2 files changed, 38 insertions(+), 23 deletions(-) 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 d80d14cc347cd..e626e0a0ea8f5 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -432,7 +432,7 @@ def _( reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...] reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...] # TODO: This should be `(str, bytes) -> int` - reveal_type(my_callable) # revealed: Unknown + reveal_type(my_callable) # revealed: (...) -> Unknown reveal_type(annotated_int) # revealed: int reveal_type(transparent_alias) # revealed: int reveal_type(optional_int) # revealed: int | None @@ -472,7 +472,7 @@ reveal_type(ListOfPairs) # revealed: reveal_type(ListOrTupleOfInts) # revealed: types.UnionType reveal_type(AnnotatedInt) # revealed: reveal_type(SubclassOfInt) # revealed: GenericAlias -reveal_type(CallableIntToStr) # revealed: Unknown +reveal_type(CallableIntToStr) # revealed: GenericAlias def _( ints_or_none: IntsOrNone, @@ -490,7 +490,7 @@ def _( reveal_type(annotated_int) # revealed: int reveal_type(subclass_of_int) # revealed: type[int] # TODO: This should be `(int, /) -> str` - reveal_type(callable_int_to_str) # revealed: Unknown + reveal_type(callable_int_to_str) # revealed: (...) -> Unknown ``` A generic implicit type alias can also be used in another generic implicit type alias: @@ -653,7 +653,7 @@ ListOfInts = list[int] # error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" def _(doubly_specialized: ListOfInts[int]): - reveal_type(doubly_specialized) # revealed: Unknown + reveal_type(doubly_specialized) # revealed: list[int] ``` Specializing a generic implicit type alias with an incorrect number of type arguments also results @@ -674,8 +674,8 @@ def _( # error: [invalid-type-arguments] "No type argument provided for required type variable `U`" dict_too_few_args: MyDict[int], ): - reveal_type(list_too_many_args) # revealed: Unknown - reveal_type(dict_too_few_args) # revealed: Unknown + reveal_type(list_too_many_args) # revealed: list[Unknown] + reveal_type(dict_too_few_args) # revealed: dict[Unknown, Unknown] ``` Trying to specialize a non-name node results in an error: @@ -693,7 +693,7 @@ def _( # error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" specialized: this_does_not_work()[int], ): - reveal_type(specialized) # revealed: Unknown + reveal_type(specialized) # revealed: int | str ``` Similarly, if you try to specialize a union type without a binding context, we emit an error: @@ -704,7 +704,7 @@ Similarly, if you try to specialize a union type without a binding context, we e x: (list[T] | set[T])[int] def _(): - reveal_type(x) # revealed: Unknown + reveal_type(x) # revealed: list[typing.TypeVar] | set[typing.TypeVar] ``` ### Multiple definitions @@ -1521,11 +1521,23 @@ def _( ### Self-referential generic implicit type aliases ```py -# TODO: uncomment these -# from typing import TypeVar -# -# T = TypeVar("T") -# NestedDict = dict[str, "NestedDict[T] | T"] -# -# NestedList = list["NestedList[T] | None"] +from typing import TypeVar + +T = TypeVar("T") + +# TODO: No errors +# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" +NestedDict = dict[str, "NestedDict[T] | T"] +# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" +NestedList = list["NestedList[T] | None"] + +def _( + # TODO: No errors + # error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" + nested_dict_int: NestedDict[int], + # error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" + nested_list_str: NestedList[str], +): + reveal_type(nested_dict_int) # revealed: dict[str, Divergent] + reveal_type(nested_list_str) # revealed: list[Divergent] ``` 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 787dfeb05c80b..b92dd0b102af4 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -184,14 +184,17 @@ def my_isinstance(obj: object, classinfo: ClassInfo) -> bool: reveal_type(classinfo) # revealed: type | UnionType | tuple[Divergent, ...] return isinstance(obj, classinfo) -# TODO: uncomment this block -# K = TypeVar("K") -# V = TypeVar("V") -# NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]] - -# def _(nested: NestedDict[str, int]): -# # TODO should be `dict[str, int | NestedDict[str, int]]` -# reveal_type(nested) # revealed: @Todo(specialized generic alias in type expression) +K = TypeVar("K") +V = TypeVar("V") +# TODO: No error here +# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" +NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]] + +# TODO: No error here +# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" +def _(nested: NestedDict[str, int]): + # TODO should be `dict[str, int | NestedDict[str, int]]` + reveal_type(nested) # revealed: dict[Unknown, Divergent] my_isinstance(1, int) my_isinstance(1, int | str) From f19f005d4d706293faf393e506d9a9b81de1afeb Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 27 Nov 2025 14:40:56 +0100 Subject: [PATCH 15/25] Workaround for explicitly specialized generic recursive implicit type aliases --- crates/ruff_benchmark/benches/ty_walltime.rs | 2 +- .../resources/mdtest/implicit_type_aliases.md | 6 ---- .../resources/mdtest/pep613_type_aliases.md | 6 +--- .../types/infer/builder/type_expression.rs | 30 +++++++++++++++++++ 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index 6c8e89de0c7a6..9f29b193a5799 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -120,7 +120,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY310, }, - 600, + 1070, ); static FREQTRADE: Benchmark = Benchmark::new( 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 e626e0a0ea8f5..e6fd32ef4e9ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -1525,17 +1525,11 @@ from typing import TypeVar T = TypeVar("T") -# TODO: No errors -# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" NestedDict = dict[str, "NestedDict[T] | T"] -# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" NestedList = list["NestedList[T] | None"] def _( - # TODO: No errors - # error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" nested_dict_int: NestedDict[int], - # error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" nested_list_str: NestedList[str], ): reveal_type(nested_dict_int) # revealed: dict[str, Divergent] 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 b92dd0b102af4..0fb5b4a43f531 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -186,15 +186,11 @@ def my_isinstance(obj: object, classinfo: ClassInfo) -> bool: K = TypeVar("K") V = TypeVar("V") -# TODO: No error here -# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]] -# TODO: No error here -# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" def _(nested: NestedDict[str, int]): # TODO should be `dict[str, int | NestedDict[str, int]]` - reveal_type(nested) # revealed: dict[Unknown, Divergent] + reveal_type(nested) # revealed: dict[@Todo(specialized recursive generic type alias), Divergent] my_isinstance(1, int) my_isinstance(1, int | str) 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 80882cbaf9f98..54d6b482f111b 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 @@ -760,6 +760,36 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let scope_id = self.scope(); let current_typevar_binding_context = self.typevar_binding_context; + + // TODO + // If we explicitly specialize a recursive generic (PEP-613 or implicit) type alias, + // we currently miscount the number of type variables. For example, for a nested + // dictionary type alias `NestedDict = dict[K, "V | NestedDict[K, V]"]]`, we might + // infer ``, and therefore count just one type variable + // instead of two. So until we properly support these, specialize all remaining type + // variables with a `@Todo` type (since we don't know which of the type arguments + // belongs to the remaining type variables). + if any_over_type(self.db(), value_ty, &|ty| ty.is_divergent(), true) { + let value_ty = value_ty.apply_specialization( + db, + generic_context.specialize( + db, + std::iter::repeat_n( + todo_type!("specialized recursive generic type alias"), + generic_context.len(db), + ) + .collect(), + ), + ); + return if in_type_expression { + value_ty + .in_type_expression(db, scope_id, current_typevar_binding_context) + .unwrap_or_else(|_| Type::unknown()) + } else { + value_ty + }; + } + let specialize = |types: &[Option>]| { let specialized = value_ty.apply_specialization( db, From bb4b8e3c78f36e1e5e249a1e23ce5ecf62a970e1 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 27 Nov 2025 21:50:06 +0100 Subject: [PATCH 16/25] Get rid of false positives for Callables --- .../resources/mdtest/implicit_type_aliases.md | 16 +++------- .../src/types/infer/builder.rs | 31 +++++++++++++++++++ .../types/infer/builder/type_expression.rs | 5 ++- 3 files changed, 40 insertions(+), 12 deletions(-) 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 e6fd32ef4e9ee..302fb384caac5 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -401,7 +401,7 @@ reveal_type(Pair) # revealed: reveal_type(Sum) # revealed: reveal_type(ListOrTuple) # revealed: types.UnionType reveal_type(ListOrTupleLegacy) # revealed: types.UnionType -reveal_type(MyCallable) # revealed: GenericAlias +reveal_type(MyCallable) # revealed: @Todo(Callable[..] specialized with ParamSpec) reveal_type(AnnotatedType) # revealed: reveal_type(TransparentAlias) # revealed: typing.TypeVar reveal_type(MyOptional) # revealed: types.UnionType @@ -415,9 +415,6 @@ def _( int_and_bytes: Sum[int, bytes], list_or_tuple: ListOrTuple[int], list_or_tuple_legacy: ListOrTupleLegacy[int], - # TODO: no errors here - # error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[str, bytes]`?" - # error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" my_callable: MyCallable[[str, bytes], int], annotated_int: AnnotatedType[int], transparent_alias: TransparentAlias[int], @@ -432,7 +429,7 @@ def _( reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...] reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...] # TODO: This should be `(str, bytes) -> int` - reveal_type(my_callable) # revealed: (...) -> Unknown + reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec) reveal_type(annotated_int) # revealed: int reveal_type(transparent_alias) # revealed: int reveal_type(optional_int) # revealed: int | None @@ -461,9 +458,6 @@ ListOfPairs = MyList[Pair[str]] ListOrTupleOfInts = ListOrTuple[int] AnnotatedInt = AnnotatedType[int] SubclassOfInt = MyType[int] -# TODO: No error here -# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" -# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `list[int]`?" CallableIntToStr = MyCallable[[int], str] reveal_type(IntsOrNone) # revealed: types.UnionType @@ -472,7 +466,7 @@ reveal_type(ListOfPairs) # revealed: reveal_type(ListOrTupleOfInts) # revealed: types.UnionType reveal_type(AnnotatedInt) # revealed: reveal_type(SubclassOfInt) # revealed: GenericAlias -reveal_type(CallableIntToStr) # revealed: GenericAlias +reveal_type(CallableIntToStr) # revealed: @Todo(Callable[..] specialized with ParamSpec) def _( ints_or_none: IntsOrNone, @@ -490,7 +484,7 @@ def _( reveal_type(annotated_int) # revealed: int reveal_type(subclass_of_int) # revealed: type[int] # TODO: This should be `(int, /) -> str` - reveal_type(callable_int_to_str) # revealed: (...) -> Unknown + reveal_type(callable_int_to_str) # revealed: @Todo(Callable[..] specialized with ParamSpec) ``` A generic implicit type alias can also be used in another generic implicit type alias: @@ -553,7 +547,7 @@ def _( # TODO: Should be `list[Unknown] | tuple[Unknown, ...]` reveal_type(list_or_tuple_legacy) # revealed: list[T@ListOrTupleLegacy] | tuple[T@ListOrTupleLegacy, ...] # TODO: Should be `(...) -> Unknown` - reveal_type(my_callable) # revealed: (...) -> T@MyCallable + reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec) # TODO: Should be `Unknown` reveal_type(annotated_unknown) # revealed: T@AnnotatedType # TODO: Should be `Unknown | None` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index fd8cdd168e2f0..d415a252ba75f 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -11088,6 +11088,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); } Type::SpecialForm(SpecialFormType::Callable) => { + // TODO: Remove this once we support ParamSpec properly. This is necessary to avoid + // a lot of false positives downstream, because we can't represent the specialized + // `Callable[P, _]` type yet. + if let Some(first_arg) = subscript + .slice + .as_ref() + .as_tuple_expr() + .and_then(|args| args.elts.first()) + && first_arg.is_name_expr() + { + let first_arg_ty = self.infer_expression(first_arg, TypeContext::default()); + + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = first_arg_ty + && typevar.kind(self.db()).is_paramspec() + { + return todo_type!("Callable[..] specialized with ParamSpec"); + } + + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "The first argument to `Callable` must be either a list of types, \ + ParamSpec, Concatenate, or `...`", + )); + } + return Type::KnownInstance(KnownInstanceType::Callable( + CallableType::unknown(self.db()) + .as_callable() + .expect("always returns Type::Callable"), + )); + } + let callable = self .infer_callable_type(subscript) .as_callable() 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 54d6b482f111b..701ce58163360 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 @@ -977,7 +977,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } }, Type::Dynamic(_) => { - self.infer_type_expression(slice); + // Infer slice as a value expression to avoid false-positive + // `invalid-type-form` diagnostics, when we have e.g. + // `MyCallable[[int, str], None]` but `MyCallable` is dynamic. + self.infer_expression(slice, TypeContext::default()); value_ty } Type::ClassLiteral(class) => { From ae781f70593c676c45b4e4a5c2943bb756f20d1e Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 28 Nov 2025 09:44:33 +0100 Subject: [PATCH 17/25] Remove type[T] Todo type --- .../resources/mdtest/implicit_type_aliases.md | 5 ++--- crates/ty_python_semantic/src/types.rs | 16 +--------------- 2 files changed, 3 insertions(+), 18 deletions(-) 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 302fb384caac5..36add0508abc6 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -509,8 +509,7 @@ def _( ): reveal_type(list_of_ints) # revealed: list[int] reveal_type(subclass_of_int) # revealed: type[int] - # TODO: Should be `type[Any] | list[Any]` - reveal_type(type_or_list) # revealed: @Todo(type[T] for typevar T) | list[Any] + reveal_type(type_or_list) # revealed: type[Any] | list[Any] ``` If a generic implicit type alias is used unspecialized in a type expression, we use the default @@ -535,7 +534,7 @@ def _( # TODO: This should be `dict[Unknown, Unknown]` reveal_type(dict_unknown) # revealed: dict[T@MyDict, U@MyDict] # TODO: Should be `type[Unknown]` - reveal_type(subclass_of_unknown) # revealed: @Todo(type[T] for typevar T) + reveal_type(subclass_of_unknown) # revealed: type[T@MyType] # TODO: Should be `tuple[int, Unknown]` reveal_type(int_and_unknown) # revealed: tuple[int, T@IntAndType] # TODO: Should be `tuple[Unknown, Unknown]` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 52e8fda6ea5c5..ff62b4f65474e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7228,21 +7228,7 @@ impl<'db> Type<'db> { // (`int` -> instance of `int` -> subclass of `int`) can be lossy, but it is // okay for all valid arguments to `type[…]`. - let ty = instance.inner(db); - - if ty.is_type_var() { - // TODO: - // This is a temporary workaround until we have proper support for type[T]. - // If we pass a typevar to `.to_meta_type()`, we currently get `type[B]`, - // where `B` is the upper bound of `T`. However, we really need `type[T]` - // here. Otherwise, when we specialize a generic implicit type alias like - // `TypeOrList[T] = type[T] | list[T]` using `TypeOrList[Any]`, we would get - // `type[B] | list[Any]`, which leads to a lot of false positives for numpy- - // users. - Ok(todo_type!("type[T] for typevar T")) - } else { - Ok(ty.to_meta_type(db)) - } + Ok(instance.inner(db).to_meta_type(db)) } KnownInstanceType::Callable(callable) => Ok(Type::Callable(*callable)), KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)), From 14a753414d5e61e7d24459e0ec125ae56427f2db Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 28 Nov 2025 10:40:52 +0100 Subject: [PATCH 18/25] formulation --- .../resources/mdtest/implicit_type_aliases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 36add0508abc6..046725cf64210 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -553,7 +553,7 @@ def _( reveal_type(optional_unknown) # revealed: T@MyOptional | None ``` -For a type variable with a default, we use that: +For a type variable with a default, we use the default type: ```py T_default = TypeVar("T_default", default=int) From 495dfe32f2a5d027902c220dc0e129cec0a46c3d Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 28 Nov 2025 17:16:01 +0100 Subject: [PATCH 19/25] Fix tests after rebase --- crates/ty_python_semantic/resources/mdtest/libraries/numpy.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md index d1afd367ca978..2b657a34f7d0d 100644 --- a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md +++ b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md @@ -44,9 +44,6 @@ class _SupportsDType(Protocol[_DTypeT_co]): @property def dtype(self) -> _DTypeT_co: ... -# TODO: no errors here -# error: [invalid-type-arguments] "Type `typing.TypeVar` is not assignable to upper bound `generic[Any]` of type variable `_ScalarT_co@dtype`" -# error: [invalid-type-arguments] "Type `typing.TypeVar` is not assignable to upper bound `generic[Any]` of type variable `_ScalarT_co@dtype`" _DTypeLike: TypeAlias = type[_ScalarT] | dtype[_ScalarT] | _SupportsDType[dtype[_ScalarT]] DTypeLike: TypeAlias = _DTypeLike[Any] | str | None From e9a1d14a9f49e1ef51ac6c418d8eaa679f7c9a7a Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 28 Nov 2025 19:53:28 +0100 Subject: [PATCH 20/25] Add tuple unpacking test case --- .../resources/mdtest/implicit_type_aliases.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 046725cf64210..4dee483f8dc77 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -637,6 +637,27 @@ def _( reveal_type(list_of_ints) # revealed: list[int] ``` +### Tuple unpacking + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import TypeVar + +T = TypeVar("T") +U = TypeVar("U") +V = TypeVar("V") + +X = tuple[T, *tuple[U, ...], V] +Y = X[T, tuple[int, str, U], bytes] + +def g(obj: Y[bool, range]): + reveal_type(obj) # revealed: tuple[bool, *tuple[tuple[int, str, range], ...], bytes] +``` + ### Error cases A generic alias that is already fully specialized cannot be specialized again: From b8ae9439e26749c5435782ecd4d8d2444c6a7252 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 28 Nov 2025 19:53:52 +0100 Subject: [PATCH 21/25] Add test case for totally stringified type aliases (which still reveals a @Todo type) --- .../resources/mdtest/pep613_type_aliases.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 0fb5b4a43f531..c3e63c7b8eaa3 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -98,6 +98,12 @@ def _(x: MyAlias): ## Generic aliases +A more comprehensive set of tests can be found in +[`implicit_type_aliases.md`](./implicit_type_aliases.md). If the implementations ever diverge, we +may need to duplicate more tests here. + +### Basic + ```py from typing import TypeAlias, TypeVar @@ -114,6 +120,21 @@ def _(list_of_int: MyList[int], list_or_set_of_str: ListOrSet[str]): reveal_type(list_or_set_of_str) # revealed: list[str] | set[str] ``` +### Stringified generic alias + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +TotallyStringifiedPEP613: TypeAlias = "dict[T, U]" +TotallyStringifiedPartiallySpecialized: TypeAlias = "TotallyStringifiedPEP613[U, int]" + +def f(x: "TotallyStringifiedPartiallySpecialized[str]"): + reveal_type(x) # revealed: @Todo(Generic stringified PEP-613 type alias) +``` + ## Subscripted generic alias in union ```py From 9a7231d24af8b03850279a5e8e53bd2e282428a2 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 28 Nov 2025 20:00:17 +0100 Subject: [PATCH 22/25] Add back TODO comment --- .../ty_python_semantic/resources/mdtest/implicit_type_aliases.md | 1 + 1 file changed, 1 insertion(+) 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 4dee483f8dc77..6cec459452130 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -667,6 +667,7 @@ ListOfInts = list[int] # error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" def _(doubly_specialized: ListOfInts[int]): + # TODO: This should ideally be `list[Unknown]` or `Unknown` reveal_type(doubly_specialized) # revealed: list[int] ``` From 267a51e3d86731667325caa78b3ea529270c3419 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 28 Nov 2025 20:00:51 +0100 Subject: [PATCH 23/25] Another TODO comment --- .../ty_python_semantic/resources/mdtest/implicit_type_aliases.md | 1 + 1 file changed, 1 insertion(+) 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 6cec459452130..532ee471eb2ca 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -719,6 +719,7 @@ Similarly, if you try to specialize a union type without a binding context, we e x: (list[T] | set[T])[int] def _(): + # TODO: `list[Unknown] | set[Unknown]` might be better reveal_type(x) # revealed: list[typing.TypeVar] | set[typing.TypeVar] ``` From 5384ea41622f7e741ebaf94619dbdffdd7e49a05 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 28 Nov 2025 20:10:08 +0100 Subject: [PATCH 24/25] Remove questionable TODO comments --- .../resources/mdtest/implicit_type_aliases.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 532ee471eb2ca..8207d55bcc22a 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -785,9 +785,11 @@ if flag(): else: MyAlias = set[T] -# TODO: This should be an error like "Invalid subscript of object of type ` | ` in type expression" +# It is questionable whether this should be supported or not. It might also be reasonable to +# emit an error here (e.g. "Invalid subscript of object of type ` | +# ` in type expression"). If we ever choose to do so, the revealed +# type should probably be `Unknown`. def _(x: MyAlias[int]): - # TODO: This should be `Unknown` reveal_type(x) # revealed: list[int] | set[int] ``` From db39fa1aa3bf931b96138ba883c1bdc2ff677e68 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 28 Nov 2025 20:10:32 +0100 Subject: [PATCH 25/25] Return CallableType from CallableType::unknown --- crates/ty_python_semantic/src/types.rs | 8 ++++---- crates/ty_python_semantic/src/types/infer/builder.rs | 4 +--- .../src/types/infer/builder/type_expression.rs | 2 +- crates/ty_python_semantic/src/types/narrow.rs | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index ff62b4f65474e..3c701de55758d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -3726,7 +3726,7 @@ impl<'db> Type<'db> { dunder_call .has_relation_to_impl( db, - CallableType::unknown(db), + Type::Callable(CallableType::unknown(db)), inferable, TypeRelation::Assignability, relation_visitor, @@ -7258,7 +7258,7 @@ impl<'db> Type<'db> { SpecialFormType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)), // TODO: Use an opt-in rule for a bare `Callable` - SpecialFormType::Callable => Ok(CallableType::unknown(db)), + SpecialFormType::Callable => Ok(Type::Callable(CallableType::unknown(db))), // Special case: `NamedTuple` in a type expression is understood to describe the type // `tuple[object, ...] & `. @@ -11684,8 +11684,8 @@ impl<'db> CallableType<'db> { } /// Create a callable type which accepts any parameters and returns an `Unknown` type. - pub(crate) fn unknown(db: &'db dyn Db) -> Type<'db> { - Type::Callable(Self::single(db, Signature::unknown())) + pub(crate) fn unknown(db: &'db dyn Db) -> CallableType<'db> { + Self::single(db, Signature::unknown()) } pub(crate) fn bind_self( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index d415a252ba75f..df165921fe119 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -11113,9 +11113,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); } return Type::KnownInstance(KnownInstanceType::Callable( - CallableType::unknown(self.db()) - .as_callable() - .expect("always returns Type::Callable"), + CallableType::unknown(self.db()), )); } 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 701ce58163360..74f6607615f02 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 @@ -1121,7 +1121,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { Type::single_callable(db, Signature::new(parameters, Some(return_type))) } else { - CallableType::unknown(db) + Type::Callable(CallableType::unknown(db)) }; // `Signature` / `Parameters` are not a `Type` variant, so we're storing diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 32ac648dc4d7b..984f214414e24 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -238,7 +238,7 @@ impl ClassInfoConstraintFunction { Type::SpecialForm(SpecialFormType::Callable) if self == ClassInfoConstraintFunction::IsInstance => { - Some(CallableType::unknown(db).top_materialization(db)) + Some(Type::Callable(CallableType::unknown(db)).top_materialization(db)) } Type::SpecialForm(special_form) => special_form