From f23d2c9b9e600cef08d6ed797cba4d335fe7772b Mon Sep 17 00:00:00 2001 From: lipefree <43332207+lipefree@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:09:51 +0200 Subject: [PATCH] [ty] Support using legacy typing aliases for generic classes in type annotations (#18404) Co-authored-by: Alex Waygood --- .../annotations/stdlib_typing_aliases.md | 78 ++++++++--- .../resources/mdtest/generics/builtins.md | 7 +- crates/ty_python_semantic/src/types/class.rs | 6 +- crates/ty_python_semantic/src/types/infer.rs | 130 +++++++++++++----- 4 files changed, 158 insertions(+), 63 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md index 0801b2d0db..990fbe33fd 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -31,41 +31,81 @@ def f( ordered_dict_parametrized: typing.OrderedDict[int, str], ): reveal_type(list_bare) # revealed: list[Unknown] - # TODO: revealed: list[int] - reveal_type(list_parametrized) # revealed: list[Unknown] + reveal_type(list_parametrized) # revealed: list[int] reveal_type(dict_bare) # revealed: dict[Unknown, Unknown] - # TODO: revealed: dict[int, str] - reveal_type(dict_parametrized) # revealed: dict[Unknown, Unknown] + reveal_type(dict_parametrized) # revealed: dict[int, str] reveal_type(set_bare) # revealed: set[Unknown] - # TODO: revealed: set[int] - reveal_type(set_parametrized) # revealed: set[Unknown] + reveal_type(set_parametrized) # revealed: set[int] - # TODO: revealed: frozenset[Unknown] reveal_type(frozen_set_bare) # revealed: frozenset[Unknown] - # TODO: revealed: frozenset[str] - reveal_type(frozen_set_parametrized) # revealed: frozenset[Unknown] + reveal_type(frozen_set_parametrized) # revealed: frozenset[str] reveal_type(chain_map_bare) # revealed: ChainMap[Unknown, Unknown] - # TODO: revealed: ChainMap[str, int] - reveal_type(chain_map_parametrized) # revealed: ChainMap[Unknown, Unknown] + reveal_type(chain_map_parametrized) # revealed: ChainMap[str, int] reveal_type(counter_bare) # revealed: Counter[Unknown] - # TODO: revealed: Counter[int] - reveal_type(counter_parametrized) # revealed: Counter[Unknown] + reveal_type(counter_parametrized) # revealed: Counter[int] reveal_type(default_dict_bare) # revealed: defaultdict[Unknown, Unknown] - # TODO: revealed: defaultdict[str, int] - reveal_type(default_dict_parametrized) # revealed: defaultdict[Unknown, Unknown] + reveal_type(default_dict_parametrized) # revealed: defaultdict[str, int] reveal_type(deque_bare) # revealed: deque[Unknown] - # TODO: revealed: deque[str] - reveal_type(deque_parametrized) # revealed: deque[Unknown] + reveal_type(deque_parametrized) # revealed: deque[str] reveal_type(ordered_dict_bare) # revealed: OrderedDict[Unknown, Unknown] - # TODO: revealed: OrderedDict[int, str] - reveal_type(ordered_dict_parametrized) # revealed: OrderedDict[Unknown, Unknown] + reveal_type(ordered_dict_parametrized) # revealed: OrderedDict[int, str] +``` + +## Incorrect number of type arguments + +In case the incorrect number of type arguments is passed, a diagnostic is given. + +```py +import typing + +def f( + # error: [invalid-type-form] "Legacy alias `typing.List` expected exactly 1 argument, got 2" + incorrect_list: typing.List[int, int], + # error: [invalid-type-form] "Legacy alias `typing.Dict` expected exactly 2 arguments, got 3" + incorrect_dict: typing.Dict[int, int, int], + # error: [invalid-type-form] "Legacy alias `typing.Dict` expected exactly 2 arguments, got 1" + incorrect_dict2: typing.Dict[int], # type argument is not a tuple here + # error: [invalid-type-form] + incorrect_set: typing.Set[int, int], + # error: [invalid-type-form] + incorrect_frozen_set: typing.FrozenSet[int, int], + # error: [invalid-type-form] + incorrect_chain_map: typing.ChainMap[int, int, int], + # error: [invalid-type-form] + incorrect_chain_map2: typing.ChainMap[int], + # error: [invalid-type-form] + incorrect_counter: typing.Counter[int, int], + # error: [invalid-type-form] + incorrect_default_dict: typing.DefaultDict[int, int, int], + # error: [invalid-type-form] + incorrect_default_dict2: typing.DefaultDict[int], + # error: [invalid-type-form] + incorrect_deque: typing.Deque[int, int], + # error: [invalid-type-form] + incorrect_ordered_dict: typing.OrderedDict[int, int, int], + # error: [invalid-type-form] + incorrect_ordered_dict2: typing.OrderedDict[int], +): + reveal_type(incorrect_list) # revealed: list[Unknown] + reveal_type(incorrect_dict) # revealed: dict[Unknown, Unknown] + reveal_type(incorrect_dict2) # revealed: dict[Unknown, Unknown] + reveal_type(incorrect_set) # revealed: set[Unknown] + reveal_type(incorrect_frozen_set) # revealed: frozenset[Unknown] + reveal_type(incorrect_chain_map) # revealed: ChainMap[Unknown, Unknown] + reveal_type(incorrect_chain_map2) # revealed: ChainMap[Unknown, Unknown] + reveal_type(incorrect_counter) # revealed: Counter[Unknown] + reveal_type(incorrect_default_dict) # revealed: defaultdict[Unknown, Unknown] + reveal_type(incorrect_default_dict2) # revealed: defaultdict[Unknown, Unknown] + reveal_type(incorrect_deque) # revealed: deque[Unknown] + reveal_type(incorrect_ordered_dict) # revealed: OrderedDict[Unknown, Unknown] + reveal_type(incorrect_ordered_dict2) # revealed: OrderedDict[Unknown, Unknown] ``` ## Inheritance diff --git a/crates/ty_python_semantic/resources/mdtest/generics/builtins.md b/crates/ty_python_semantic/resources/mdtest/generics/builtins.md index 70b9c91902..e98de384ab 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/builtins.md @@ -24,12 +24,13 @@ class dict[K, V, Extra]: ... def reveal_type(obj, /): ... ``` -If we don't, then we won't be able to infer the types of variadic keyword arguments correctly. +If we don't, then we may get "surprising" results when inferring the types of variadic keyword +arguments. ```py def f(**kwargs): - reveal_type(kwargs) # revealed: Unknown + reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown] def g(**kwargs: int): - reveal_type(kwargs) # revealed: Unknown + reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown] ``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 50b9e8b682..94158e653c 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2426,7 +2426,7 @@ impl<'db> KnownClass { return Type::unknown(); }; let Some(generic_context) = class_literal.generic_context(db) else { - return Type::unknown(); + return Type::instance(db, ClassType::NonGeneric(class_literal)); }; let types = specialization.into_iter().collect::>(); @@ -2437,11 +2437,11 @@ impl<'db> KnownClass { if MESSAGES.lock().unwrap().insert(self) { tracing::info!( "Wrong number of types when specializing {}. \ - Falling back to `Unknown` for the symbol instead.", + Falling back to default specialization for the symbol instead.", self.display(db) ); } - return Type::unknown(); + return Type::instance(db, class_literal.default_specialization(db)); } let specialization = generic_context.specialize(db, types); diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 2120b4a944..44b5d06616 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -7975,7 +7975,7 @@ impl<'db> TypeInferenceBuilder<'db> { match value_ty { Type::SpecialForm(SpecialFormType::Annotated) => { - // This branch is similar to the corresponding branch in `infer_parameterized_known_instance_type_expression`, but + // This branch is similar to the corresponding branch in `infer_parameterized_special_form_type_expression`, but // `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly // differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`). if let ast::Expr::Tuple(ast::ExprTuple { @@ -8701,6 +8701,43 @@ impl<'db> TypeInferenceBuilder<'db> { } } + fn infer_parameterized_legacy_typing_alias( + &mut self, + subscript_node: &ast::ExprSubscript, + expected_arg_count: usize, + alias: SpecialFormType, + class: KnownClass, + ) -> Type<'db> { + let arguments = &*subscript_node.slice; + let (args, args_number) = if let ast::Expr::Tuple(t) = arguments { + (Either::Left(t), t.len()) + } else { + (Either::Right([arguments]), 1) + }; + if args_number != expected_arg_count { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript_node) { + let noun = if expected_arg_count == 1 { + "argument" + } else { + "arguments" + }; + builder.into_diagnostic(format_args!( + "Legacy alias `{alias}` expected exactly {expected_arg_count} {noun}, \ + got {args_number}", + )); + } + } + let ty = class.to_specialized_instance( + self.db(), + args.into_iter() + .map(|node| self.infer_type_expression(node)), + ); + if arguments.is_tuple_expr() { + self.store_expression_type(arguments, ty); + } + ty + } + fn infer_parameterized_special_form_type_expression( &mut self, subscript: &ast::ExprSubscript, @@ -8916,43 +8953,60 @@ impl<'db> TypeInferenceBuilder<'db> { } }, - // TODO: Generics - SpecialFormType::ChainMap => { - self.infer_type_expression(arguments_slice); - KnownClass::ChainMap.to_instance(db) - } - SpecialFormType::OrderedDict => { - self.infer_type_expression(arguments_slice); - KnownClass::OrderedDict.to_instance(db) - } - SpecialFormType::Dict => { - self.infer_type_expression(arguments_slice); - KnownClass::Dict.to_instance(db) - } - SpecialFormType::List => { - self.infer_type_expression(arguments_slice); - KnownClass::List.to_instance(db) - } - SpecialFormType::DefaultDict => { - self.infer_type_expression(arguments_slice); - KnownClass::DefaultDict.to_instance(db) - } - SpecialFormType::Counter => { - self.infer_type_expression(arguments_slice); - KnownClass::Counter.to_instance(db) - } - SpecialFormType::Set => { - self.infer_type_expression(arguments_slice); - KnownClass::Set.to_instance(db) - } - SpecialFormType::FrozenSet => { - self.infer_type_expression(arguments_slice); - KnownClass::FrozenSet.to_instance(db) - } - SpecialFormType::Deque => { - self.infer_type_expression(arguments_slice); - KnownClass::Deque.to_instance(db) - } + SpecialFormType::ChainMap => self.infer_parameterized_legacy_typing_alias( + subscript, + 2, + SpecialFormType::ChainMap, + KnownClass::ChainMap, + ), + SpecialFormType::OrderedDict => self.infer_parameterized_legacy_typing_alias( + subscript, + 2, + SpecialFormType::OrderedDict, + KnownClass::OrderedDict, + ), + SpecialFormType::Dict => self.infer_parameterized_legacy_typing_alias( + subscript, + 2, + SpecialFormType::Dict, + KnownClass::Dict, + ), + SpecialFormType::List => self.infer_parameterized_legacy_typing_alias( + subscript, + 1, + SpecialFormType::List, + KnownClass::List, + ), + SpecialFormType::DefaultDict => self.infer_parameterized_legacy_typing_alias( + subscript, + 2, + SpecialFormType::DefaultDict, + KnownClass::DefaultDict, + ), + SpecialFormType::Counter => self.infer_parameterized_legacy_typing_alias( + subscript, + 1, + SpecialFormType::Counter, + KnownClass::Counter, + ), + SpecialFormType::Set => self.infer_parameterized_legacy_typing_alias( + subscript, + 1, + SpecialFormType::Set, + KnownClass::Set, + ), + SpecialFormType::FrozenSet => self.infer_parameterized_legacy_typing_alias( + subscript, + 1, + SpecialFormType::FrozenSet, + KnownClass::FrozenSet, + ), + SpecialFormType::Deque => self.infer_parameterized_legacy_typing_alias( + subscript, + 1, + SpecialFormType::Deque, + KnownClass::Deque, + ), SpecialFormType::ReadOnly => { self.infer_type_expression(arguments_slice);