From 66e9d57797cc24b12286e823e84ee4e2968b41e2 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 14 Nov 2025 09:08:58 +0100 Subject: [PATCH] [ty] Support legacy `typing` special forms in implicit type aliases (#21433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Support various legacy `typing` special forms (`List`, `Dict`, …) in implicit type aliases. ## Ecosystem impact A lot of true positives (e.g. on `alerta`)! ## Test Plan New Markdown tests --- .../resources/mdtest/implicit_type_aliases.md | 226 +++++++++++++++++- .../src/types/infer/builder.rs | 89 +++++++ 2 files changed, 307 insertions(+), 8 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 fd0610e1aa..a06b3c65f0 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -680,8 +680,21 @@ def _( Invalid uses result in diagnostics: ```py +from typing import Literal + # error: [invalid-type-form] -InvalidSubclass = type[1] +InvalidSubclassOf1 = type[1] + +# TODO: This should be an error +InvalidSubclassOfLiteral = type[Literal[42]] + +def _( + invalid_subclass_of_1: InvalidSubclassOf1, + invalid_subclass_of_literal: InvalidSubclassOfLiteral, +): + reveal_type(invalid_subclass_of_1) # revealed: type[Unknown] + # TODO: this should be `type[Unknown]` or `Unknown` + reveal_type(invalid_subclass_of_literal) # revealed: ``` ### `Type[…]` @@ -759,6 +772,178 @@ Invalid uses result in diagnostics: InvalidSubclass = Type[1] ``` +## Other `typing` special forms + +The following special forms from the `typing` module are also supported in implicit type aliases: + +```py +from typing import List, Dict, Set, FrozenSet, ChainMap, Counter, DefaultDict, Deque, OrderedDict + +MyList = List[str] +MySet = Set[str] +MyDict = Dict[str, int] +MyFrozenSet = FrozenSet[str] +MyChainMap = ChainMap[str, int] +MyCounter = Counter[str] +MyDefaultDict = DefaultDict[str, int] +MyDeque = Deque[str] +MyOrderedDict = OrderedDict[str, int] + +reveal_type(MyList) # revealed: +reveal_type(MySet) # revealed: +reveal_type(MyDict) # revealed: +reveal_type(MyFrozenSet) # revealed: +reveal_type(MyChainMap) # revealed: +reveal_type(MyCounter) # revealed: +reveal_type(MyDefaultDict) # revealed: +reveal_type(MyDeque) # revealed: +reveal_type(MyOrderedDict) # revealed: + +def _( + my_list: MyList, + my_set: MySet, + my_dict: MyDict, + my_frozen_set: MyFrozenSet, + my_chain_map: MyChainMap, + my_counter: MyCounter, + my_default_dict: MyDefaultDict, + my_deque: MyDeque, + my_ordered_dict: MyOrderedDict, +): + reveal_type(my_list) # revealed: list[str] + reveal_type(my_set) # revealed: set[str] + reveal_type(my_dict) # revealed: dict[str, int] + reveal_type(my_frozen_set) # revealed: frozenset[str] + reveal_type(my_chain_map) # revealed: ChainMap[str, int] + reveal_type(my_counter) # revealed: Counter[str] + reveal_type(my_default_dict) # revealed: defaultdict[str, int] + reveal_type(my_deque) # revealed: deque[str] + reveal_type(my_ordered_dict) # revealed: OrderedDict[str, int] +``` + +All of them are supported in unions: + +```py +NoneOrList = None | List[str] +NoneOrSet = None | Set[str] +NoneOrDict = None | Dict[str, int] +NoneOrFrozenSet = None | FrozenSet[str] +NoneOrChainMap = None | ChainMap[str, int] +NoneOrCounter = None | Counter[str] +NoneOrDefaultDict = None | DefaultDict[str, int] +NoneOrDeque = None | Deque[str] +NoneOrOrderedDict = None | OrderedDict[str, int] + +ListOrNone = List[int] | None +SetOrNone = Set[int] | None +DictOrNone = Dict[str, int] | None +FrozenSetOrNone = FrozenSet[int] | None +ChainMapOrNone = ChainMap[str, int] | None +CounterOrNone = Counter[str] | None +DefaultDictOrNone = DefaultDict[str, int] | None +DequeOrNone = Deque[str] | None +OrderedDictOrNone = OrderedDict[str, int] | None + +reveal_type(NoneOrList) # revealed: types.UnionType +reveal_type(NoneOrSet) # revealed: types.UnionType +reveal_type(NoneOrDict) # revealed: types.UnionType +reveal_type(NoneOrFrozenSet) # revealed: types.UnionType +reveal_type(NoneOrChainMap) # revealed: types.UnionType +reveal_type(NoneOrCounter) # revealed: types.UnionType +reveal_type(NoneOrDefaultDict) # revealed: types.UnionType +reveal_type(NoneOrDeque) # revealed: types.UnionType +reveal_type(NoneOrOrderedDict) # revealed: types.UnionType + +reveal_type(ListOrNone) # revealed: types.UnionType +reveal_type(SetOrNone) # revealed: types.UnionType +reveal_type(DictOrNone) # revealed: types.UnionType +reveal_type(FrozenSetOrNone) # revealed: types.UnionType +reveal_type(ChainMapOrNone) # revealed: types.UnionType +reveal_type(CounterOrNone) # revealed: types.UnionType +reveal_type(DefaultDictOrNone) # revealed: types.UnionType +reveal_type(DequeOrNone) # revealed: types.UnionType +reveal_type(OrderedDictOrNone) # revealed: types.UnionType + +def _( + none_or_list: NoneOrList, + none_or_set: NoneOrSet, + none_or_dict: NoneOrDict, + none_or_frozen_set: NoneOrFrozenSet, + none_or_chain_map: NoneOrChainMap, + none_or_counter: NoneOrCounter, + none_or_default_dict: NoneOrDefaultDict, + none_or_deque: NoneOrDeque, + none_or_ordered_dict: NoneOrOrderedDict, + list_or_none: ListOrNone, + set_or_none: SetOrNone, + dict_or_none: DictOrNone, + frozen_set_or_none: FrozenSetOrNone, + chain_map_or_none: ChainMapOrNone, + counter_or_none: CounterOrNone, + default_dict_or_none: DefaultDictOrNone, + deque_or_none: DequeOrNone, + ordered_dict_or_none: OrderedDictOrNone, +): + reveal_type(none_or_list) # revealed: None | list[str] + reveal_type(none_or_set) # revealed: None | set[str] + reveal_type(none_or_dict) # revealed: None | dict[str, int] + reveal_type(none_or_frozen_set) # revealed: None | frozenset[str] + reveal_type(none_or_chain_map) # revealed: None | ChainMap[str, int] + reveal_type(none_or_counter) # revealed: None | Counter[str] + reveal_type(none_or_default_dict) # revealed: None | defaultdict[str, int] + reveal_type(none_or_deque) # revealed: None | deque[str] + reveal_type(none_or_ordered_dict) # revealed: None | OrderedDict[str, int] + + reveal_type(list_or_none) # revealed: list[int] | None + reveal_type(set_or_none) # revealed: set[int] | None + reveal_type(dict_or_none) # revealed: dict[str, int] | None + reveal_type(frozen_set_or_none) # revealed: frozenset[int] | None + reveal_type(chain_map_or_none) # revealed: ChainMap[str, int] | None + reveal_type(counter_or_none) # revealed: Counter[str] | None + reveal_type(default_dict_or_none) # revealed: defaultdict[str, int] | None + reveal_type(deque_or_none) # revealed: deque[str] | None + reveal_type(ordered_dict_or_none) # revealed: OrderedDict[str, int] | None +``` + +Invalid uses result in diagnostics: + +```py +from typing import List, Dict + +# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" +InvalidList = List[1] + +# error: [invalid-type-form] "`typing.typing.List` requires exactly one argument" +ListTooManyArgs = List[int, str] + +# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" +InvalidDict1 = Dict[1, str] + +# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" +InvalidDict2 = Dict[str, 2] + +# error: [invalid-type-form] "`typing.typing.Dict` requires exactly two arguments, got 1" +DictTooFewArgs = Dict[str] + +# error: [invalid-type-form] "`typing.typing.Dict` requires exactly two arguments, got 3" +DictTooManyArgs = Dict[str, int, float] + +def _( + invalid_list: InvalidList, + list_too_many_args: ListTooManyArgs, + invalid_dict1: InvalidDict1, + invalid_dict2: InvalidDict2, + dict_too_few_args: DictTooFewArgs, + dict_too_many_args: DictTooManyArgs, +): + reveal_type(invalid_list) # revealed: list[Unknown] + reveal_type(list_too_many_args) # revealed: list[Unknown] + reveal_type(invalid_dict1) # revealed: dict[Unknown, str] + reveal_type(invalid_dict2) # revealed: dict[str, Unknown] + reveal_type(dict_too_few_args) # revealed: dict[str, Unknown] + reveal_type(dict_too_many_args) # revealed: dict[Unknown, Unknown] +``` + ## Stringified annotations? From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html): @@ -789,22 +974,28 @@ We *do* support stringified annotations if they appear in a position where a typ syntactically expected: ```py -from typing import Union +from typing import Union, List, Dict -ListOfInts = list["int"] +ListOfInts1 = list["int"] +ListOfInts2 = List["int"] StrOrStyle = Union[str, "Style"] SubclassOfStyle = type["Style"] +DictStrToStyle = Dict[str, "Style"] class Style: ... def _( - list_of_ints: ListOfInts, + list_of_ints1: ListOfInts1, + list_of_ints2: ListOfInts2, str_or_style: StrOrStyle, subclass_of_style: SubclassOfStyle, + dict_str_to_style: DictStrToStyle, ): - reveal_type(list_of_ints) # revealed: list[int] + reveal_type(list_of_ints1) # revealed: list[int] + reveal_type(list_of_ints2) # revealed: list[int] reveal_type(str_or_style) # revealed: str | Style reveal_type(subclass_of_style) # revealed: type[Style] + reveal_type(dict_str_to_style) # revealed: dict[str, Style] ``` ## Recursive @@ -828,8 +1019,27 @@ python-version = "3.12" ``` ```py -Recursive = list["Recursive" | None] +from typing import List, Dict -def _(r: Recursive): - reveal_type(r) # revealed: list[Divergent] +RecursiveList1 = list["RecursiveList1" | None] +RecursiveList2 = List["RecursiveList2" | None] +RecursiveDict1 = dict[str, "RecursiveDict1" | None] +RecursiveDict2 = Dict[str, "RecursiveDict2" | None] +RecursiveDict3 = dict["RecursiveDict3", int] +RecursiveDict4 = Dict["RecursiveDict4", int] + +def _( + recursive_list1: RecursiveList1, + recursive_list2: RecursiveList2, + recursive_dict1: RecursiveDict1, + recursive_dict2: RecursiveDict2, + recursive_dict3: RecursiveDict3, + recursive_dict4: RecursiveDict4, +): + reveal_type(recursive_list1) # revealed: list[Divergent] + reveal_type(recursive_list2) # revealed: list[Divergent] + reveal_type(recursive_dict1) # revealed: dict[str, Divergent] + reveal_type(recursive_dict2) # revealed: dict[str, Divergent] + reveal_type(recursive_dict3) # revealed: dict[Divergent, int] + reveal_type(recursive_dict4) # revealed: dict[Divergent, int] ``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 34c333df0e..05c6193060 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10779,6 +10779,95 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { InternedType::new(self.db(), argument_ty), )); } + // `typing` special forms with a single generic argument + Type::SpecialForm( + special_form @ (SpecialFormType::List + | SpecialFormType::Set + | SpecialFormType::FrozenSet + | SpecialFormType::Counter + | SpecialFormType::Deque), + ) => { + let slice_ty = self.infer_type_expression(slice); + + let element_ty = if matches!(**slice, ast::Expr::Tuple(_)) { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "`typing.{}` requires exactly one argument", + special_form.repr() + )); + } + Type::unknown() + } else { + slice_ty + }; + + let class = special_form + .aliased_stdlib_class() + .expect("A known stdlib class is available"); + + return class + .to_specialized_class_type(self.db(), [element_ty]) + .map(Type::from) + .unwrap_or_else(Type::unknown); + } + // `typing` special forms with two generic arguments + Type::SpecialForm( + special_form @ (SpecialFormType::Dict + | SpecialFormType::ChainMap + | SpecialFormType::DefaultDict + | SpecialFormType::OrderedDict), + ) => { + let (first_ty, second_ty) = if let ast::Expr::Tuple(ast::ExprTuple { + elts: ref arguments, + .. + }) = **slice + { + if arguments.len() != 2 { + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( + "`typing.{}` requires exactly two arguments, got {}", + special_form.repr(), + arguments.len() + )); + } + } + + if let [first_expr, second_expr] = &arguments[..] { + let first_ty = self.infer_type_expression(first_expr); + let second_ty = self.infer_type_expression(second_expr); + + (first_ty, second_ty) + } else { + for argument in arguments { + self.infer_type_expression(argument); + } + + (Type::unknown(), Type::unknown()) + } + } else { + let first_ty = self.infer_type_expression(slice); + + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "`typing.{}` requires exactly two arguments, got 1", + special_form.repr() + )); + } + + (first_ty, Type::unknown()) + }; + + let class = special_form + .aliased_stdlib_class() + .expect("Stdlib class available"); + + return class + .to_specialized_class_type(self.db(), [first_ty, second_ty]) + .map(Type::from) + .unwrap_or_else(Type::unknown); + } _ => {} }