From e4833614c2235b9b8d8a81221ba9e402ac3999a9 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 2 Dec 2025 20:12:13 +0100 Subject: [PATCH] [ty] Default specialize generic type aliases --- .../resources/mdtest/implicit_type_aliases.md | 45 ++++++----------- crates/ty_python_semantic/src/types.rs | 31 ++++++++++-- .../infer/builder/annotation_expression.rs | 33 +++++++----- .../types/infer/builder/type_expression.rs | 50 +++++++++++-------- 4 files changed, 92 insertions(+), 67 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 8207d55bcc..5f89c53bc5 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -190,14 +190,10 @@ def _( reveal_type(type_of_str_or_int) # revealed: type[str] | int 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: T@TypeVarOrInt | int - # TODO should be int | Unknown - reveal_type(int_or_type_var) # revealed: int | T@IntOrTypeVar - # TODO should be Unknown | None - reveal_type(type_var_or_none) # revealed: T@TypeVarOrNone | None - # TODO should be None | Unknown - reveal_type(none_or_type_var) # revealed: None | T@NoneOrTypeVar + reveal_type(type_var_or_int) # revealed: Unknown | int + reveal_type(int_or_type_var) # revealed: int | Unknown + reveal_type(type_var_or_none) # revealed: Unknown | None + reveal_type(none_or_type_var) # revealed: None | Unknown ``` If a type is unioned with itself in a value expression, the result is just that type. No @@ -529,28 +525,18 @@ def _( annotated_unknown: AnnotatedType, optional_unknown: MyOptional, ): - # 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: 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]` - 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, ...] + reveal_type(list_unknown) # revealed: list[Unknown] + reveal_type(dict_unknown) # revealed: dict[Unknown, Unknown] + reveal_type(subclass_of_unknown) # revealed: type[Unknown] + reveal_type(int_and_unknown) # revealed: tuple[int, Unknown] + reveal_type(pair_of_unknown) # revealed: tuple[Unknown, Unknown] + reveal_type(unknown_and_unknown) # revealed: tuple[Unknown, Unknown] + reveal_type(list_or_tuple) # revealed: list[Unknown] | tuple[Unknown, ...] + reveal_type(list_or_tuple_legacy) # revealed: list[Unknown] | tuple[Unknown, ...] # TODO: Should be `(...) -> Unknown` 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` - reveal_type(optional_unknown) # revealed: T@MyOptional | None + reveal_type(annotated_unknown) # revealed: Unknown + reveal_type(optional_unknown) # revealed: Unknown | None ``` For a type variable with a default, we use the default type: @@ -565,8 +551,7 @@ def _( 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] + reveal_type(list_of_int) # revealed: list[int] ``` (Generic) implicit type aliases can be used as base classes: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 7c65a4715e..6195f5be28 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7540,6 +7540,15 @@ impl<'db> Type<'db> { self.apply_type_mapping_impl(db, type_mapping, tcx, &ApplyTypeMappingVisitor::default()) } + /// Erase all free type variables in this type, replacing them with their defaults + /// or `Unknown` if no default exists. + /// + /// This is used when an implicit type alias containing free type variables is used + /// in a type expression without explicit type arguments. + pub(crate) fn erase_free_typevars(self, db: &'db dyn Db) -> Type<'db> { + self.apply_type_mapping(db, &TypeMapping::EraseTypevars, TypeContext::default()) + } + fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, @@ -7571,7 +7580,8 @@ impl<'db> Type<'db> { TypeMapping::ReplaceSelf { .. } | TypeMapping::Materialize(_) | TypeMapping::ReplaceParameterDefaults | - TypeMapping::EagerExpansion => self, + TypeMapping::EagerExpansion | + TypeMapping::EraseTypevars => self, } } KnownInstanceType::UnionType(instance) => { @@ -7748,6 +7758,7 @@ impl<'db> Type<'db> { TypeMapping::Materialize(_) | TypeMapping::ReplaceParameterDefaults | TypeMapping::EagerExpansion | + TypeMapping::EraseTypevars | TypeMapping::PromoteLiterals(PromoteLiteralsMode::Off) => self, TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => self.promote_literals_impl(db, tcx) } @@ -7760,7 +7771,8 @@ impl<'db> Type<'db> { TypeMapping::ReplaceSelf { .. } | TypeMapping::PromoteLiterals(_) | TypeMapping::ReplaceParameterDefaults | - TypeMapping::EagerExpansion => self, + TypeMapping::EagerExpansion | + TypeMapping::EraseTypevars => self, TypeMapping::Materialize(materialization_kind) => match materialization_kind { MaterializationKind::Top => Type::object(), MaterializationKind::Bottom => Type::Never, @@ -8434,6 +8446,9 @@ pub enum TypeMapping<'a, 'db> { /// Apply eager expansion to the type. /// In the case of recursive type aliases, this will diverge, so that part will be replaced with `Divergent`. EagerExpansion, + /// Replace all type variables with `Unknown`. This is used when an implicit type alias containing + /// free type variables is used in a type expression without explicit type arguments. + EraseTypevars, } impl<'db> TypeMapping<'_, 'db> { @@ -8450,7 +8465,8 @@ impl<'db> TypeMapping<'_, 'db> { | TypeMapping::BindLegacyTypevars(_) | TypeMapping::Materialize(_) | TypeMapping::ReplaceParameterDefaults - | TypeMapping::EagerExpansion => context, + | TypeMapping::EagerExpansion + | TypeMapping::EraseTypevars => context, TypeMapping::BindSelf { .. } => GenericContext::from_typevar_instances( db, context @@ -8487,7 +8503,8 @@ impl<'db> TypeMapping<'_, 'db> { | TypeMapping::BindSelf { .. } | TypeMapping::ReplaceSelf { .. } | TypeMapping::ReplaceParameterDefaults - | TypeMapping::EagerExpansion => self.clone(), + | TypeMapping::EagerExpansion + | TypeMapping::EraseTypevars => self.clone(), } } } @@ -9866,6 +9883,12 @@ impl<'db> BoundTypeVarInstance<'db> { | TypeMapping::ReplaceParameterDefaults | TypeMapping::BindLegacyTypevars(_) | TypeMapping::EagerExpansion => Type::TypeVar(self), + TypeMapping::EraseTypevars => { + // Replace the type variable with its default, or Unknown if no default exists. + self.default_type(db) + .unwrap_or_else(Type::unknown) + .apply_type_mapping(db, type_mapping, TypeContext::default()) + } TypeMapping::Materialize(materialization_kind) => { Type::TypeVar(self.materialize_impl(db, *materialization_kind, visitor)) } diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 8c211735cd..b53047c3b4 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -143,20 +143,27 @@ impl<'db> TypeInferenceBuilder<'db, '_> { TypeQualifiers::INIT_VAR, ) } - _ => TypeAndQualifiers::declared( - ty.in_type_expression( - builder.db(), - builder.scope(), - builder.typevar_binding_context, + _ => { + // When a name or attribute expression resolves to a type containing free + // type variables (like a GenericAlias from an implicit type alias), we need + // to erase them since the alias is being used without explicit type arguments. + let erased = ty.erase_free_typevars(builder.db()); + TypeAndQualifiers::declared( + erased + .in_type_expression( + builder.db(), + builder.scope(), + builder.typevar_binding_context, + ) + .unwrap_or_else(|error| { + error.into_fallback_type( + &builder.context, + annotation, + builder.is_reachable(annotation), + ) + }), ) - .unwrap_or_else(|error| { - error.into_fallback_type( - &builder.context, - annotation, - builder.is_reachable(annotation), - ) - }), - ), + } } } 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 0e3a326139..41c5a48e60 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 @@ -89,16 +89,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-type_expression match expression { ast::Expr::Name(name) => match name.ctx { - ast::ExprContext::Load => self - .infer_name_expression(name) - .in_type_expression(self.db(), self.scope(), self.typevar_binding_context) - .unwrap_or_else(|error| { - error.into_fallback_type( - &self.context, - expression, - self.is_reachable(expression), - ) - }), + ast::ExprContext::Load => { + // When a name expression resolves to a type containing free type variables + // (like a GenericAlias from an implicit type alias), we need to erase them + // since the alias is being used without explicit type arguments. + let ty = self.infer_name_expression(name); + let erased = ty.erase_free_typevars(self.db()); + erased + .in_type_expression(self.db(), self.scope(), self.typevar_binding_context) + .unwrap_or_else(|error| { + error.into_fallback_type( + &self.context, + expression, + self.is_reachable(expression), + ) + }) + } ast::ExprContext::Invalid => Type::unknown(), ast::ExprContext::Store | ast::ExprContext::Del => { todo_type!("Name expression annotation in Store/Del context") @@ -106,16 +112,20 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }, ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx { - ast::ExprContext::Load => self - .infer_attribute_expression(attribute_expression) - .in_type_expression(self.db(), self.scope(), self.typevar_binding_context) - .unwrap_or_else(|error| { - error.into_fallback_type( - &self.context, - expression, - self.is_reachable(expression), - ) - }), + ast::ExprContext::Load => { + // Same as name expressions - erase free type variables in attribute access + let ty = self.infer_attribute_expression(attribute_expression); + let erased = ty.erase_free_typevars(self.db()); + erased + .in_type_expression(self.db(), self.scope(), self.typevar_binding_context) + .unwrap_or_else(|error| { + error.into_fallback_type( + &self.context, + expression, + self.is_reachable(expression), + ) + }) + } ast::ExprContext::Invalid => Type::unknown(), ast::ExprContext::Store | ast::ExprContext::Del => { todo_type!("Attribute expression annotation in Store/Del context")