[ty] Generic implicit types aliases

This commit is contained in:
David Peter 2025-11-21 08:48:32 +01:00
parent dd15656deb
commit 013d43a2dd
5 changed files with 216 additions and 93 deletions

View File

@ -79,9 +79,8 @@ async def main():
task("B"), task("B"),
) )
# TODO: these should be `int` reveal_type(a) # revealed: int
reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: int
reveal_type(b) # revealed: Unknown
``` ```
## Under the hood ## Under the hood

View File

@ -388,6 +388,8 @@ ListOrTuple = list[T] | tuple[T, ...]
ListOrTupleLegacy = Union[list[T], tuple[T, ...]] ListOrTupleLegacy = Union[list[T], tuple[T, ...]]
MyCallable = Callable[P, T] MyCallable = Callable[P, T]
AnnotatedType = Annotated[T, "tag"] AnnotatedType = Annotated[T, "tag"]
TransparentAlias = T
MyOptional = T | None
# TODO: Consider displaying this as `<class 'list[T]'>`, … instead? (and similar for some others below) # TODO: Consider displaying this as `<class 'list[T]'>`, … instead? (and similar for some others below)
reveal_type(MyList) # revealed: <class 'list[typing.TypeVar]'> reveal_type(MyList) # revealed: <class 'list[typing.TypeVar]'>
@ -400,43 +402,40 @@ reveal_type(ListOrTuple) # revealed: types.UnionType
reveal_type(ListOrTupleLegacy) # revealed: types.UnionType reveal_type(ListOrTupleLegacy) # revealed: types.UnionType
reveal_type(MyCallable) # revealed: GenericAlias reveal_type(MyCallable) # revealed: GenericAlias
reveal_type(AnnotatedType) # revealed: <typing.Annotated special form> reveal_type(AnnotatedType) # revealed: <typing.Annotated special form>
reveal_type(TransparentAlias) # revealed: typing.TypeVar
reveal_type(MyOptional) # revealed: types.UnionType
def _( def _(
list_of_ints: MyList[int], list_of_ints: MyList[int],
dict_str_to_int: MyDict[str, 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], subclass_of_int: MyType[int],
int_and_str: IntAndType[str], int_and_str: IntAndType[str],
pair_of_ints: Pair[int], pair_of_ints: Pair[int],
int_and_bytes: Sum[int, bytes], int_and_bytes: Sum[int, bytes],
list_or_tuple: ListOrTuple[int], list_or_tuple: ListOrTuple[int],
list_or_tuple_legacy: ListOrTupleLegacy[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: [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], my_callable: MyCallable[[str, bytes], int],
annotated_int: AnnotatedType[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: list[int]
reveal_type(list_of_ints) # revealed: @Todo(specialized generic alias in type expression) reveal_type(dict_str_to_int) # revealed: dict[str, int]
# TODO: This should be `dict[str, int]` reveal_type(subclass_of_int) # revealed: type[int]
reveal_type(dict_str_to_int) # revealed: @Todo(specialized generic alias in type expression) reveal_type(int_and_str) # revealed: tuple[int, str]
# TODO: This should be `type[int]` reveal_type(pair_of_ints) # revealed: tuple[int, int]
reveal_type(subclass_of_int) # revealed: Unknown reveal_type(int_and_bytes) # revealed: tuple[int, bytes]
# TODO: This should be `tuple[int, str]` reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...]
reveal_type(int_and_str) # revealed: @Todo(specialized generic alias in type expression) reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...]
# TODO: This should be `tuple[int, int]` reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[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)
# TODO: This should be `(str, bytes) -> int` # TODO: This should be `(str, bytes) -> int`
reveal_type(my_callable) # revealed: @Todo(Generic specialization of typing.Callable) reveal_type(my_callable) # revealed: Unknown
# TODO: This should be `int` reveal_type(annotated_int) # revealed: int
reveal_type(annotated_int) # revealed: @Todo(Generic specialization of typing.Annotated) reveal_type(transparent_alias) # revealed: int
reveal_type(optional_int) # revealed: int | None
``` ```
Generic implicit type aliases can be partially specialized: Generic implicit type aliases can be partially specialized:
@ -446,15 +445,12 @@ U = TypeVar("U")
DictStrTo = MyDict[str, U] DictStrTo = MyDict[str, U]
reveal_type(DictStrTo) # revealed: GenericAlias reveal_type(DictStrTo) # revealed: <class 'dict[str, typing.TypeVar]'>
def _( 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], dict_str_to_int: DictStrTo[int],
): ):
# TODO: This should be `dict[str, int]` reveal_type(dict_str_to_int) # revealed: dict[str, int]
reveal_type(dict_str_to_int) # revealed: Unknown
``` ```
Using specializations of generic implicit type aliases in other implicit type aliases works as 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] IntsOrStrs = Pair[int] | Pair[str]
ListOfPairs = MyList[Pair[str]] ListOfPairs = MyList[Pair[str]]
reveal_type(IntsOrNone) # revealed: UnionType reveal_type(IntsOrNone) # revealed: types.UnionType
reveal_type(IntsOrStrs) # revealed: UnionType reveal_type(IntsOrStrs) # revealed: types.UnionType
reveal_type(ListOfPairs) # revealed: GenericAlias reveal_type(ListOfPairs) # revealed: <class 'list[tuple[str, str]]'>
def _( 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, 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, ints_or_strs: IntsOrStrs,
list_of_pairs: ListOfPairs, list_of_pairs: ListOfPairs,
): ):
# TODO: This should be `list[int] | None` reveal_type(ints_or_none) # revealed: list[int] | None
reveal_type(ints_or_none) # revealed: Unknown reveal_type(ints_or_strs) # revealed: tuple[int, int] | tuple[str, str]
# TODO: This should be `tuple[int, int] | tuple[str, str]` reveal_type(list_of_pairs) # revealed: list[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) A generic implicit type alias can also be used in another generic implicit type alias:
```py
MyOtherList = MyList[T]
reveal_type(MyOtherList) # revealed: <class 'list[typing.TypeVar]'>
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 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] GenericBaseAlias = GenericBase[T]
# TODO: No error here
# error: [non-subscriptable] "Cannot subscript object of type `<class 'GenericBase[typing.TypeVar]'>` with no `__class_getitem__` method"
class Derived2(GenericBaseAlias[int]): class Derived2(GenericBaseAlias[int]):
pass pass
``` ```
@ -533,10 +533,9 @@ A generic alias that is already fully specialized cannot be specialized again:
```py ```py
ListOfInts = list[int] 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]): def _(doubly_specialized: ListOfInts[int]):
# TODO: this should be `Unknown` reveal_type(doubly_specialized) # revealed: Unknown
reveal_type(doubly_specialized) # revealed: @Todo(specialized generic alias in type expression)
``` ```
Specializing a generic implicit type alias with an incorrect number of type arguments also results Specializing a generic implicit type alias with an incorrect number of type arguments also results
@ -544,15 +543,13 @@ in an error:
```py ```py
def _( 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], 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], dict_too_few_args: MyDict[int],
): ):
# TODO: this should be `Unknown` reveal_type(list_too_many_args) # revealed: Unknown
reveal_type(list_too_many_args) # revealed: @Todo(specialized generic alias in type expression) reveal_type(dict_too_few_args) # revealed: Unknown
# TODO: this should be `Unknown`
reveal_type(dict_too_few_args) # revealed: @Todo(specialized generic alias in type expression)
``` ```
## `Literal`s ## `Literal`s
@ -642,8 +639,7 @@ Deprecated = Annotated[T, "deprecated attribute"]
class C: class C:
old: Deprecated[int] old: Deprecated[int]
# TODO: Should be `int` reveal_type(C().old) # revealed: int
reveal_type(C().old) # revealed: @Todo(Generic specialization of typing.Annotated)
``` ```
If the metadata argument is missing, we emit an error (because this code fails at runtime), but If the metadata argument is missing, we emit an error (because this code fails at runtime), but

View File

@ -7198,7 +7198,9 @@ impl<'db> Type<'db> {
} }
} }
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => match type_mapping { Type::KnownInstance(known_instance) => match known_instance {
KnownInstanceType::TypeVar(typevar) => {
match type_mapping {
TypeMapping::BindLegacyTypevars(binding_context) => { TypeMapping::BindLegacyTypevars(binding_context) => {
Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context)) Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context))
} }
@ -7210,6 +7212,57 @@ impl<'db> Type<'db> {
TypeMapping::Materialize(_) | TypeMapping::Materialize(_) |
TypeMapping::ReplaceParameterDefaults => self, TypeMapping::ReplaceParameterDefaults => 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) => { Type::FunctionLiteral(function) => {
let function = Type::FunctionLiteral(function.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); let function = Type::FunctionLiteral(function.apply_type_mapping_impl(db, type_mapping, tcx, visitor));
@ -7369,8 +7422,7 @@ impl<'db> Type<'db> {
// some other generic context's specialization is applied to it. // some other generic context's specialization is applied to it.
| Type::ClassLiteral(_) | Type::ClassLiteral(_)
| Type::BoundSuper(_) | Type::BoundSuper(_)
| Type::SpecialForm(_) | Type::SpecialForm(_) => self,
| Type::KnownInstance(_) => self,
} }
} }
@ -7507,6 +7559,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::Dynamic(_)
| Type::Never | Type::Never
| Type::AlwaysTruthy | Type::AlwaysTruthy
@ -7534,7 +7624,6 @@ impl<'db> Type<'db> {
| Type::EnumLiteral(_) | Type::EnumLiteral(_)
| Type::BoundSuper(_) | Type::BoundSuper(_)
| Type::SpecialForm(_) | Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::TypedDict(_) => {} | Type::TypedDict(_) => {}
} }
} }

View File

@ -10794,6 +10794,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> {
let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); 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) self.infer_subscript_load_impl(value_ty, subscript)
} }

View File

@ -2,6 +2,7 @@ use itertools::Either;
use ruff_python_ast as ast; use ruff_python_ast as ast;
use super::{DeferredExpressionState, TypeInferenceBuilder}; use super::{DeferredExpressionState, TypeInferenceBuilder};
use crate::FxOrderSet;
use crate::types::diagnostic::{ use crate::types::diagnostic::{
self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form,
report_invalid_arguments_to_callable, report_invalid_arguments_to_callable,
@ -11,9 +12,9 @@ use crate::types::string_annotation::parse_string_annotation;
use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::tuple::{TupleSpecBuilder, TupleType};
use crate::types::visitor::any_over_type; use crate::types::visitor::any_over_type;
use crate::types::{ use crate::types::{
CallableType, DynamicType, IntersectionBuilder, KnownClass, KnownInstanceType, BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, KnownClass,
LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType, Type, KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType,
TypeAliasType, TypeContext, TypeIsType, UnionBuilder, UnionType, todo_type, Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, todo_type,
}; };
/// Type expressions /// Type expressions
@ -750,6 +751,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<Type<'db>>]| {
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( fn infer_subscript_type_expression(
&mut self, &mut self,
subscript: &ast::ExprSubscript, subscript: &ast::ExprSubscript,
@ -840,10 +884,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
Type::unknown() Type::unknown()
} }
KnownInstanceType::TypeVar(_) => {
self.infer_type_expression(slice);
todo_type!("TypeVar annotations")
}
KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => { KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => {
match type_alias.generic_context(self.db()) { match type_alias.generic_context(self.db()) {
Some(generic_context) => { Some(generic_context) => {
@ -886,11 +926,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_type_expression(slice); self.infer_type_expression(slice);
todo_type!("Generic stringified PEP-613 type alias") todo_type!("Generic stringified PEP-613 type alias")
} }
KnownInstanceType::UnionType(_) => { KnownInstanceType::Literal(ty) => {
self.infer_type_expression(slice);
todo_type!("Generic specialization of types.UnionType")
}
KnownInstanceType::Literal(ty) | KnownInstanceType::TypeGenericAlias(ty) => {
self.infer_type_expression(slice); self.infer_type_expression(slice);
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!( builder.into_diagnostic(format_args!(
@ -900,13 +936,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
Type::unknown() Type::unknown()
} }
KnownInstanceType::Callable(_) => { KnownInstanceType::TypeVar(_) => self
self.infer_type_expression(slice); .infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, false),
todo_type!("Generic specialization of typing.Callable")
} KnownInstanceType::UnionType(_)
KnownInstanceType::Annotated(_) => { | KnownInstanceType::Callable(_)
self.infer_type_expression(slice); | KnownInstanceType::Annotated(_)
todo_type!("Generic specialization of typing.Annotated") | KnownInstanceType::TypeGenericAlias(_) => {
self.infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, true)
} }
KnownInstanceType::NewType(newtype) => { KnownInstanceType::NewType(newtype) => {
self.infer_type_expression(&subscript.slice); self.infer_type_expression(&subscript.slice);
@ -949,11 +986,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
} }
Type::GenericAlias(_) => { Type::GenericAlias(_) => {
self.infer_type_expression(slice); self.infer_explicitly_specialized_implicit_type_alias(subscript, value_ty, true)
// 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")
} }
Type::StringLiteral(_) => { Type::StringLiteral(_) => {
self.infer_type_expression(slice); self.infer_type_expression(slice);