mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 03:36:18 +00:00
[ty] Support legacy typing special forms in implicit type aliases (#21433)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
## 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
This commit is contained in:
parent
87dafb8787
commit
66e9d57797
2 changed files with 307 additions and 8 deletions
|
|
@ -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: <class 'int'>
|
||||
```
|
||||
|
||||
### `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: <class 'list[str]'>
|
||||
reveal_type(MySet) # revealed: <class 'set[str]'>
|
||||
reveal_type(MyDict) # revealed: <class 'dict[str, int]'>
|
||||
reveal_type(MyFrozenSet) # revealed: <class 'frozenset[str]'>
|
||||
reveal_type(MyChainMap) # revealed: <class 'ChainMap[str, int]'>
|
||||
reveal_type(MyCounter) # revealed: <class 'Counter[str]'>
|
||||
reveal_type(MyDefaultDict) # revealed: <class 'defaultdict[str, int]'>
|
||||
reveal_type(MyDeque) # revealed: <class 'deque[str]'>
|
||||
reveal_type(MyOrderedDict) # revealed: <class 'OrderedDict[str, int]'>
|
||||
|
||||
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]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue