[ty] Generic "manual" PEP 695 type aliases

This commit is contained in:
David Peter 2025-12-04 22:12:09 +01:00
parent fdcb5a7e73
commit e700c86857
7 changed files with 278 additions and 98 deletions

View File

@ -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]

View File

@ -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]

View File

@ -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

View File

@ -12898,6 +12898,8 @@ pub struct ManualPEP695TypeAliasType<'db> {
pub name: ast::name::Name,
pub definition: Option<Definition<'db>>,
pub value: Type<'db>,
generic_context: Option<GenericContext<'db>>,
specialization: Option<Specialization<'db>>,
}
// 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<GenericContext<'db>> {
// TODO: Add support for generic non-PEP695 type aliases.
match self {
TypeAliasType::PEP695(type_alias) => type_alias.generic_context(db),
TypeAliasType::ManualPEP695(_) => None,
TypeAliasType::ManualPEP695(type_alias) => type_alias.generic_context(db),
}
}
pub(crate) fn specialization(self, db: &'db dyn Db) -> Option<Specialization<'db>> {
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))
}
}
}
}

View File

@ -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,
)),
)));
}
_ => {}
}
}

View File

@ -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));

View File

@ -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")