diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index fc5aa9aded..cd59d79ad5 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -740,8 +740,26 @@ mod tests { "#, ); - // TODO: This should jump to the definition of `Alias` above. - assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> main.py:4:1 + | + 2 | from typing_extensions import TypeAliasType + 3 | + 4 | Alias = TypeAliasType("Alias", tuple[int, int]) + | ^^^^^ + 5 | + 6 | Alias + | + info: Source + --> main.py:6:1 + | + 4 | Alias = TypeAliasType("Alias", tuple[int, int]) + 5 | + 6 | Alias + | ^^^^^ + | + "#); } #[test] 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 69f92dda94..f300d98926 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -217,8 +217,7 @@ MyList = TypeAliasType("MyList", list[T], type_params=(T,)) MyAlias5 = Callable[[MyList[T]], int] def _(c: MyAlias5[int]): - # TODO: should be (list[int], /) -> int - reveal_type(c) # revealed: (Unknown, /) -> int + reveal_type(c) # revealed: (list[int], /) -> int K = TypeVar("K") V = TypeVar("V") @@ -228,14 +227,12 @@ MyDict = TypeAliasType("MyDict", dict[K, V], type_params=(K, V)) MyAlias6 = Callable[[MyDict[K, V]], int] def _(c: MyAlias6[str, bytes]): - # TODO: should be (dict[str, bytes], /) -> int - reveal_type(c) # revealed: (Unknown, /) -> int + reveal_type(c) # revealed: (dict[str, bytes], /) -> int ListOrDict: TypeAlias = MyList[T] | dict[str, T] def _(x: ListOrDict[int]): - # TODO: should be list[int] | dict[str, int] - reveal_type(x) # revealed: Unknown | dict[str, int] + reveal_type(x) # revealed: list[int] | dict[str, int] MyAlias7: TypeAlias = Callable[Concatenate[T, ...], None] diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 799d1fc36f..56ef005385 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -223,8 +223,27 @@ T = TypeVar("T") IntAndT = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,)) def f(x: IntAndT[str]) -> None: - # TODO: This should be `tuple[int, str]` - reveal_type(x) # revealed: Unknown + reveal_type(x) # revealed: tuple[int, str] + +U = TypeVar("U", default=str) + +ListOrSet = TypeAliasType("ListOrSet", list[U] | set[U], type_params=(U,)) + +def g( + list_or_set_of_int: ListOrSet[int], + list_or_set_of_str: ListOrSet, +) -> None: + reveal_type(list_or_set_of_int) # revealed: list[int] | set[int] + reveal_type(list_or_set_of_str) # revealed: list[str] | set[str] + +MyDict = TypeAliasType("MyDict", dict[U, T], type_params=(U, T)) + +def h( + dict_int_str: MyDict[int, str], + dict_str_unknown: MyDict, +) -> None: + reveal_type(dict_int_str) # revealed: dict[int, str] + reveal_type(dict_str_unknown) # revealed: dict[str, Unknown] ``` ### Error cases @@ -241,6 +260,35 @@ def get_name() -> str: IntOrStr = TypeAliasType(get_name(), int | str) ``` +#### Type parameters argument is not a tuple + +```py +from typing_extensions import TypeAliasType, TypeVar + +T = TypeVar("T") + +# error: [invalid-type-alias-type] "`type_params` argument to `TypeAliasType` must be a tuple" +IntAndT = TypeAliasType("IntAndT", tuple[int, T], type_params=T) + +def _(x: IntAndT[str]) -> None: + reveal_type(x) # revealed: Unknown +``` + +#### Invalid type parameters entries + +```py +from typing_extensions import TypeAliasType, TypeVar + +T = TypeVar("T") + +# TODO: This should be an error +IntAndT = TypeAliasType("IntAndT", tuple[int, T], type_params=(str,)) + +# error: [non-subscriptable] "Cannot subscript non-generic type alias" +def _(x: IntAndT[str]) -> None: + reveal_type(x) # revealed: Unknown +``` + ## Cyclic aliases ### Self-referential diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 81d056f91a..532250ce04 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -12898,6 +12898,8 @@ pub struct ManualPEP695TypeAliasType<'db> { pub name: ast::name::Name, pub definition: Option>, pub value: Type<'db>, + generic_context: Option>, + specialization: Option>, } // The Salsa heap is tracked separately. @@ -12912,12 +12914,50 @@ fn walk_manual_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( } impl<'db> ManualPEP695TypeAliasType<'db> { + pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + self.apply_function_specialization(db, self.value(db)) + } + + fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + if let Some(generic_context) = self.generic_context(db) { + let specialization = self + .specialization(db) + .unwrap_or_else(|| generic_context.default_specialization(db, None)); + ty.apply_specialization(db, specialization) + } else { + ty + } + } + + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + ) -> ManualPEP695TypeAliasType<'db> { + match self.generic_context(db) { + None => self, + Some(generic_context) => { + let specialization = f(generic_context); + ManualPEP695TypeAliasType::new( + db, + self.name(db), + self.definition(db), + self.value(db), + self.generic_context(db), + Some(specialization), + ) + } + } + } + fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { Self::new( db, self.name(db), self.definition(db), self.value(db).normalized_impl(db, visitor), + self.generic_context(db), + self.specialization(db), ) } @@ -12929,6 +12969,8 @@ impl<'db> ManualPEP695TypeAliasType<'db> { self.definition(db), self.value(db) .recursive_type_normalized_impl(db, div, true)?, + self.generic_context(db), + self.specialization(db), )) } } @@ -12999,7 +13041,7 @@ impl<'db> TypeAliasType<'db> { pub fn value_type(self, db: &'db dyn Db) -> Type<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.value_type(db), - TypeAliasType::ManualPEP695(type_alias) => type_alias.value(db), + TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), } } @@ -13018,24 +13060,25 @@ impl<'db> TypeAliasType<'db> { } pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { - // TODO: Add support for generic non-PEP695 type aliases. match self { TypeAliasType::PEP695(type_alias) => type_alias.generic_context(db), - TypeAliasType::ManualPEP695(_) => None, + TypeAliasType::ManualPEP695(type_alias) => type_alias.generic_context(db), } } pub(crate) fn specialization(self, db: &'db dyn Db) -> Option> { match self { TypeAliasType::PEP695(type_alias) => type_alias.specialization(db), - TypeAliasType::ManualPEP695(_) => None, + TypeAliasType::ManualPEP695(type_alias) => type_alias.specialization(db), } } fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.apply_function_specialization(db, ty), - TypeAliasType::ManualPEP695(_) => ty, + TypeAliasType::ManualPEP695(type_alias) => { + type_alias.apply_function_specialization(db, ty) + } } } @@ -13048,7 +13091,9 @@ impl<'db> TypeAliasType<'db> { TypeAliasType::PEP695(type_alias) => { TypeAliasType::PEP695(type_alias.apply_specialization(db, f)) } - TypeAliasType::ManualPEP695(_) => self, + TypeAliasType::ManualPEP695(type_alias) => { + TypeAliasType::ManualPEP695(type_alias.apply_specialization(db, f)) + } } } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 06f91502fc..6e395cdebe 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -19,7 +19,7 @@ use crate::semantic_index::{ use crate::types::bound_super::BoundSuperError; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::context::InferContext; -use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD}; +use crate::types::diagnostic::SUPER_CALL_IN_NAMED_TUPLE_METHOD; use crate::types::enums::enum_metadata; use crate::types::function::{DataclassTransformerParams, KnownFunction}; use crate::types::generics::{ @@ -35,9 +35,9 @@ use crate::types::{ ApplyTypeMappingVisitor, Binding, BoundSuperType, CallableType, CallableTypes, DATACLASS_FLAGS, DataclassFlags, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownInstanceType, - ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType, - StringLiteralType, TypeAliasType, TypeContext, TypeMapping, TypeRelation, TypedDictParams, - UnionBuilder, VarianceInferable, binding_type, declaration_type, determine_upper_bound, + MaterializationKind, NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeContext, + TypeMapping, TypeRelation, TypedDictParams, UnionBuilder, VarianceInferable, binding_type, + declaration_type, determine_upper_bound, }; use crate::{ Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, @@ -5664,42 +5664,6 @@ impl KnownClass { ))); } - KnownClass::TypeAliasType => { - let assigned_to = index - .try_expression(ast::ExprRef::from(call_expression)) - .and_then(|expr| expr.assigned_to(db)); - - let containing_assignment = assigned_to.as_ref().and_then(|assigned_to| { - match assigned_to.node(module).targets.as_slice() { - [ast::Expr::Name(target)] => Some(index.expect_single_definition(target)), - _ => None, - } - }); - - let [Some(name), Some(value), ..] = overload.parameter_types() else { - return; - }; - - let Some(name) = name.as_string_literal() else { - if let Some(builder) = - context.report_lint(&INVALID_TYPE_ALIAS_TYPE, call_expression) - { - builder.into_diagnostic( - "The name of a `typing.TypeAlias` must be a string literal", - ); - } - return; - }; - overload.set_return_type(Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( - db, - ast::name::Name::new(name.value(db)), - containing_assignment, - value, - )), - ))); - } - _ => {} } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 9488d2270d..92f7925ef7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -61,12 +61,12 @@ use crate::types::diagnostic::{ INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, - INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, - INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, - IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, - POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, - UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, - UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, + INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ALIAS_TYPE, + INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, + INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, + POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, + SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, + UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, @@ -105,13 +105,13 @@ use crate::types::visitor::any_over_type; use crate::types::{ BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypes, ClassLiteral, ClassType, DataclassParams, DynamicType, InternedType, IntersectionBuilder, - IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, - MetaclassCandidate, PEP695TypeAliasType, ParameterForm, SpecialFormType, SubclassOfType, - TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, - TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, - TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, - TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types, - overrides, todo_type, + IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, + ManualPEP695TypeAliasType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, + ParameterForm, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type, + TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity, + TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, + UnionTypeInstance, binding_type, infer_scope_types, overrides, todo_type, }; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::{EvaluationMode, UnpackPosition}; @@ -4843,6 +4843,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(KnownClass::NewType) => { self.infer_newtype_expression(target, call_expr, definition) } + Some(KnownClass::TypeAliasType) => { + self.infer_type_alias_type_expression(target, call_expr, definition) + } Some(_) | None => { self.infer_call_expression_impl(call_expr, callable_type, tcx) } @@ -5373,6 +5376,129 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ))) } + /// Infer a `TypeAliasType(name, value, type_params=(...))` call expression. + fn infer_type_alias_type_expression( + &mut self, + target: &ast::Expr, + call_expr: &ast::ExprCall, + definition: Definition<'db>, + ) -> Type<'db> { + fn error<'db>( + context: &InferContext<'db, '_>, + message: impl std::fmt::Display, + node: impl Ranged, + ) -> Type<'db> { + if let Some(builder) = context.report_lint(&INVALID_TYPE_ALIAS_TYPE, node) { + builder.into_diagnostic(message); + } + Type::unknown() + } + + let db = self.db(); + let arguments = &call_expr.arguments; + + // Extract positional arguments: name, value + let positional_args: Vec<_> = arguments + .args + .iter() + .filter(|arg| !arg.is_starred_expr()) + .collect(); + + if positional_args.len() < 2 { + return error( + &self.context, + format!( + "Wrong number of arguments in `TypeAliasType` creation, expected at least 2, found {}", + positional_args.len() + ), + call_expr, + ); + } + + // First argument: name (string literal) + let name_param_ty = self.infer_expression(positional_args[0], TypeContext::default()); + let Some(name) = name_param_ty.as_string_literal().map(|n| n.value(db)) else { + return error( + &self.context, + "The name of a `typing.TypeAlias` must be a string literal", + positional_args[0], + ); + }; + + // Validate that the target is a simple name and matches + let ast::Expr::Name(ast::ExprName { + id: target_name, .. + }) = target + else { + return error( + &self.context, + "A `TypeAliasType` definition must be a simple variable assignment", + target, + ); + }; + + if name != target_name { + return error( + &self.context, + format_args!( + "The name of a `TypeAliasType` (`{name}`) must match \ + the name of the variable it is assigned to (`{target_name}`)" + ), + target, + ); + } + + // Second argument: value + let value_ty = self.infer_type_expression(positional_args[1]); + + // Optional keyword argument: type_params + let generic_context = if let Some(type_params) = arguments + .keywords + .iter() + .find(|kw| kw.arg.as_ref().is_some_and(|arg| arg.id == "type_params")) + { + let type_params_ty = self.infer_expression(&type_params.value, TypeContext::default()); + let Some(tuple_spec) = type_params_ty.tuple_instance_spec(db) else { + return error( + &self.context, + "`type_params` argument to `TypeAliasType` must be a tuple", + &type_params.value, + ); + }; + + let mut bound_typevars = tuple_spec + .all_elements() + .filter_map(|element| { + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = element { + Some(typevar.with_binding_context(db, definition)) + } else { + // TODO: Emit a diagnostic if this is not a valid TypeVar, ParamSpec, or TypeVarTuple. + None + } + }) + .peekable(); + + if bound_typevars.peek().is_some() { + Some(GenericContext::from_typevar_instances(db, bound_typevars)) + } else { + None + } + } else { + None + }; + + Type::KnownInstance(KnownInstanceType::TypeAliasType( + TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( + db, + ast::name::Name::new(name), + Some(definition), + value_ty, + generic_context, + None, + )), + )) + } + fn infer_assignment_deferred(&mut self, value: &ast::Expr) { // Infer deferred bounds/constraints/defaults of a legacy TypeVar / ParamSpec / NewType. let ast::Expr::Call(ast::ExprCall { @@ -11020,19 +11146,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } } - Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695( - _, - ))) => { - let slice_ty = self.infer_expression(slice, TypeContext::default()); - let mut variables = FxOrderSet::default(); - slice_ty.bind_and_find_all_legacy_typevars( - self.db(), - self.typevar_binding_context, - &mut variables, - ); - let generic_context = GenericContext::from_typevar_instances(self.db(), variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); - } Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { if let Some(generic_context) = type_alias.generic_context(self.db()) { return self.infer_explicit_type_alias_type_specialization( @@ -11042,6 +11155,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { generic_context, ); } + + self.infer_expression(slice, TypeContext::default()); + if let Some(builder) = self.context.report_lint(&NON_SUBSCRIPTABLE, subscript) { + builder + .into_diagnostic(format_args!("Cannot subscript non-generic type alias")); + } + + return Type::unknown(); } Type::SpecialForm(SpecialFormType::Tuple) => { return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); 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 56b0db5a09..d732b1bb98 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 @@ -14,8 +14,8 @@ use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::visitor::any_over_type; use crate::types::{ BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, KnownClass, - KnownInstanceType, LintDiagnosticGuard, SpecialFormType, SubclassOfType, Type, TypeAliasType, - TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, todo_type, + KnownInstanceType, LintDiagnosticGuard, SpecialFormType, SubclassOfType, Type, TypeContext, + TypeIsType, TypeMapping, UnionBuilder, UnionType, todo_type, }; /// Type expressions @@ -911,7 +911,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => { + KnownInstanceType::TypeAliasType(type_alias) => { match type_alias.generic_context(self.db()) { Some(generic_context) => { let specialized_type_alias = self @@ -945,19 +945,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } } - KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695(_)) => { - // TODO: support generic "manual" PEP 695 type aliases - let slice_ty = self.infer_expression(slice, TypeContext::default()); - let mut variables = FxOrderSet::default(); - slice_ty.bind_and_find_all_legacy_typevars( - self.db(), - self.typevar_binding_context, - &mut variables, - ); - let generic_context = - GenericContext::from_typevar_instances(self.db(), variables); - Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) - } KnownInstanceType::LiteralStringAlias(_) => { self.infer_type_expression(slice); todo_type!("Generic stringified PEP-613 type alias")