Skip to content

Commit e84d523

Browse files
authored
[ty] Infer more precise types for collection literals (#20360)
## Summary Part of astral-sh/ty#168. Infer more precise types for collection literals (currently, only `list` and `set`). For example, ```py x = [1, 2, 3] # revealed: list[Unknown | int] y: list[int] = [1, 2, 3] # revealed: list[int] ``` This could easily be extended to `dict` literals, but I am intentionally limiting scope for now.
1 parent bfb0902 commit e84d523

File tree

16 files changed

+340
-77
lines changed

16 files changed

+340
-77
lines changed

crates/ty_python_semantic/resources/mdtest/assignment/annotations.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,78 @@ b: tuple[int] = ("foo",)
7979
c: tuple[str | int, str] = ([], "foo")
8080
```
8181

82+
## Collection literal annotations are understood
83+
84+
```toml
85+
[environment]
86+
python-version = "3.12"
87+
```
88+
89+
```py
90+
import typing
91+
92+
a: list[int] = [1, 2, 3]
93+
reveal_type(a) # revealed: list[int]
94+
95+
b: list[int | str] = [1, 2, 3]
96+
reveal_type(b) # revealed: list[int | str]
97+
98+
c: typing.List[int] = [1, 2, 3]
99+
reveal_type(c) # revealed: list[int]
100+
101+
d: list[typing.Any] = []
102+
reveal_type(d) # revealed: list[Any]
103+
104+
e: set[int] = {1, 2, 3}
105+
reveal_type(e) # revealed: set[int]
106+
107+
f: set[int | str] = {1, 2, 3}
108+
reveal_type(f) # revealed: set[int | str]
109+
110+
g: typing.Set[int] = {1, 2, 3}
111+
reveal_type(g) # revealed: set[int]
112+
113+
h: list[list[int]] = [[], [42]]
114+
reveal_type(h) # revealed: list[list[int]]
115+
116+
i: list[typing.Any] = [1, 2, "3", ([4],)]
117+
reveal_type(i) # revealed: list[Any | int | str | tuple[list[Unknown | int]]]
118+
119+
j: list[tuple[str | int, ...]] = [(1, 2), ("foo", "bar"), ()]
120+
reveal_type(j) # revealed: list[tuple[str | int, ...]]
121+
122+
k: list[tuple[list[int], ...]] = [([],), ([1, 2], [3, 4]), ([5], [6], [7])]
123+
reveal_type(k) # revealed: list[tuple[list[int], ...]]
124+
125+
l: tuple[list[int], *tuple[list[typing.Any], ...], list[str]] = ([1, 2, 3], [4, 5, 6], [7, 8, 9], ["10", "11", "12"])
126+
reveal_type(l) # revealed: tuple[list[int], list[Any | int], list[Any | int], list[str]]
127+
128+
type IntList = list[int]
129+
130+
m: IntList = [1, 2, 3]
131+
reveal_type(m) # revealed: list[int]
132+
133+
# TODO: this should type-check and avoid literal promotion
134+
# error: [invalid-assignment] "Object of type `list[int]` is not assignable to `list[Literal[1, 2, 3]]`"
135+
n: list[typing.Literal[1, 2, 3]] = [1, 2, 3]
136+
reveal_type(n) # revealed: list[Literal[1, 2, 3]]
137+
138+
# TODO: this should type-check and avoid literal promotion
139+
# error: [invalid-assignment] "Object of type `list[str]` is not assignable to `list[LiteralString]`"
140+
o: list[typing.LiteralString] = ["a", "b", "c"]
141+
reveal_type(o) # revealed: list[LiteralString]
142+
```
143+
144+
## Incorrect collection literal assignments are complained aobut
145+
146+
```py
147+
# error: [invalid-assignment] "Object of type `list[int]` is not assignable to `list[str]`"
148+
a: list[str] = [1, 2, 3]
149+
150+
# error: [invalid-assignment] "Object of type `set[int | str]` is not assignable to `set[int]`"
151+
b: set[int] = {1, 2, "3"}
152+
```
153+
82154
## PEP-604 annotations are supported
83155

84156
```py

crates/ty_python_semantic/resources/mdtest/del.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def delete():
4646
del d # error: [unresolved-reference] "Name `d` used when not defined"
4747

4848
delete()
49-
reveal_type(d) # revealed: list[@Todo(list literal element type)]
49+
reveal_type(d) # revealed: list[Unknown | int]
5050

5151
def delete_element():
5252
# When the `del` target isn't a name, it doesn't force local resolution.
@@ -62,7 +62,7 @@ def delete_global():
6262

6363
delete_global()
6464
# Again, the variable should have been removed, but we don't check it.
65-
reveal_type(d) # revealed: list[@Todo(list literal element type)]
65+
reveal_type(d) # revealed: list[Unknown | int]
6666

6767
def delete_nonlocal():
6868
e = 2

crates/ty_python_semantic/resources/mdtest/import/dunder_all.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -783,9 +783,8 @@ class A: ...
783783
```py
784784
from subexporter import *
785785

786-
# TODO: Should be `list[str]`
787786
# TODO: Should we avoid including `Unknown` for this case?
788-
reveal_type(__all__) # revealed: Unknown | list[@Todo(list literal element type)]
787+
reveal_type(__all__) # revealed: Unknown | list[Unknown | str]
789788

790789
__all__.append("B")
791790

crates/ty_python_semantic/resources/mdtest/literal/collections/list.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,33 @@
33
## Empty list
44

55
```py
6-
reveal_type([]) # revealed: list[@Todo(list literal element type)]
6+
reveal_type([]) # revealed: list[Unknown]
7+
```
8+
9+
## List of tuples
10+
11+
```py
12+
reveal_type([(1, 2), (3, 4)]) # revealed: list[Unknown | tuple[int, int]]
13+
```
14+
15+
## List of functions
16+
17+
```py
18+
def a(_: int) -> int:
19+
return 0
20+
21+
def b(_: int) -> int:
22+
return 1
23+
24+
x = [a, b]
25+
reveal_type(x) # revealed: list[Unknown | ((_: int) -> int)]
26+
```
27+
28+
## Mixed list
29+
30+
```py
31+
# revealed: list[Unknown | int | tuple[int, int] | tuple[int, int, int]]
32+
reveal_type([1, (1, 2), (1, 2, 3)])
733
```
834

935
## List comprehensions

crates/ty_python_semantic/resources/mdtest/literal/collections/set.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,33 @@
33
## Basic set
44

55
```py
6-
reveal_type({1, 2}) # revealed: set[@Todo(set literal element type)]
6+
reveal_type({1, 2}) # revealed: set[Unknown | int]
7+
```
8+
9+
## Set of tuples
10+
11+
```py
12+
reveal_type({(1, 2), (3, 4)}) # revealed: set[Unknown | tuple[int, int]]
13+
```
14+
15+
## Set of functions
16+
17+
```py
18+
def a(_: int) -> int:
19+
return 0
20+
21+
def b(_: int) -> int:
22+
return 1
23+
24+
x = {a, b}
25+
reveal_type(x) # revealed: set[Unknown | ((_: int) -> int)]
26+
```
27+
28+
## Mixed set
29+
30+
```py
31+
# revealed: set[Unknown | int | tuple[int, int] | tuple[int, int, int]]
32+
reveal_type({1, (1, 2), (1, 2, 3)})
733
```
834

935
## Set comprehensions

crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -310,26 +310,21 @@ no longer valid in the inner lazy scope.
310310
def f(l: list[str | None]):
311311
if l[0] is not None:
312312
def _():
313-
# TODO: should be `str | None`
314-
reveal_type(l[0]) # revealed: str | None | @Todo(list literal element type)
315-
# TODO: should be of type `list[None]`
313+
reveal_type(l[0]) # revealed: str | None | Unknown
316314
l = [None]
317315

318316
def f(l: list[str | None]):
319317
l[0] = "a"
320318
def _():
321-
# TODO: should be `str | None`
322-
reveal_type(l[0]) # revealed: str | None | @Todo(list literal element type)
323-
# TODO: should be of type `list[None]`
319+
reveal_type(l[0]) # revealed: str | None | Unknown
324320
l = [None]
325321

326322
def f(l: list[str | None]):
327323
l[0] = "a"
328324
def _():
329325
l: list[str | None] = [None]
330326
def _():
331-
# TODO: should be `str | None`
332-
reveal_type(l[0]) # revealed: @Todo(list literal element type)
327+
reveal_type(l[0]) # revealed: str | None
333328

334329
def _():
335330
def _():

crates/ty_python_semantic/resources/mdtest/subscript/lists.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ A list can be indexed into with:
99

1010
```py
1111
x = [1, 2, 3]
12-
reveal_type(x) # revealed: list[@Todo(list literal element type)]
12+
reveal_type(x) # revealed: list[Unknown | int]
1313

14-
# TODO reveal int
15-
reveal_type(x[0]) # revealed: @Todo(list literal element type)
14+
reveal_type(x[0]) # revealed: Unknown | int
1615

17-
# TODO reveal list[int]
18-
reveal_type(x[0:1]) # revealed: list[@Todo(list literal element type)]
16+
reveal_type(x[0:1]) # revealed: list[Unknown | int]
1917

2018
# error: [invalid-argument-type]
2119
reveal_type(x["a"]) # revealed: Unknown

crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never], bb: LiskovUncom
5555

5656
reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]]
5757

58-
# TODO: should be `tuple[Literal[1], ...]`
59-
reveal_type(tuple([1])) # revealed: tuple[@Todo(list literal element type), ...]
58+
reveal_type(tuple([1])) # revealed: tuple[Unknown | int, ...]
6059

6160
# error: [invalid-argument-type]
6261
reveal_type(tuple[int]([1])) # revealed: tuple[int]

crates/ty_python_semantic/resources/mdtest/unpacking.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,8 @@ reveal_type(d) # revealed: Literal[2]
213213

214214
```py
215215
a, b = [1, 2]
216-
# TODO: should be `int` for both `a` and `b`
217-
reveal_type(a) # revealed: @Todo(list literal element type)
218-
reveal_type(b) # revealed: @Todo(list literal element type)
216+
reveal_type(a) # revealed: Unknown | int
217+
reveal_type(b) # revealed: Unknown | int
219218
```
220219

221220
### Simple unpacking

crates/ty_python_semantic/src/types.rs

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,11 +1130,30 @@ impl<'db> Type<'db> {
11301130
Type::IntLiteral(_) => Some(KnownClass::Int.to_instance(db)),
11311131
Type::BytesLiteral(_) => Some(KnownClass::Bytes.to_instance(db)),
11321132
Type::ModuleLiteral(_) => Some(KnownClass::ModuleType.to_instance(db)),
1133+
Type::FunctionLiteral(_) => Some(KnownClass::FunctionType.to_instance(db)),
11331134
Type::EnumLiteral(literal) => Some(literal.enum_class_instance(db)),
11341135
_ => None,
11351136
}
11361137
}
11371138

1139+
/// If this type is a literal, promote it to a type that this literal is an instance of.
1140+
///
1141+
/// Note that this function tries to promote literals to a more user-friendly form than their
1142+
/// fallback instance type. For example, `def _() -> int` is promoted to `Callable[[], int]`,
1143+
/// as opposed to `FunctionType`.
1144+
pub(crate) fn literal_promotion_type(self, db: &'db dyn Db) -> Option<Type<'db>> {
1145+
match self {
1146+
Type::StringLiteral(_) | Type::LiteralString => Some(KnownClass::Str.to_instance(db)),
1147+
Type::BooleanLiteral(_) => Some(KnownClass::Bool.to_instance(db)),
1148+
Type::IntLiteral(_) => Some(KnownClass::Int.to_instance(db)),
1149+
Type::BytesLiteral(_) => Some(KnownClass::Bytes.to_instance(db)),
1150+
Type::ModuleLiteral(_) => Some(KnownClass::ModuleType.to_instance(db)),
1151+
Type::EnumLiteral(literal) => Some(literal.enum_class_instance(db)),
1152+
Type::FunctionLiteral(literal) => Some(Type::Callable(literal.into_callable_type(db))),
1153+
_ => None,
1154+
}
1155+
}
1156+
11381157
/// Return a "normalized" version of `self` that ensures that equivalent types have the same Salsa ID.
11391158
///
11401159
/// A normalized type:
@@ -1704,18 +1723,13 @@ impl<'db> Type<'db> {
17041723
| Type::IntLiteral(_)
17051724
| Type::BytesLiteral(_)
17061725
| Type::ModuleLiteral(_)
1707-
| Type::EnumLiteral(_),
1726+
| Type::EnumLiteral(_)
1727+
| Type::FunctionLiteral(_),
17081728
_,
17091729
) => (self.literal_fallback_instance(db)).when_some_and(|instance| {
17101730
instance.has_relation_to_impl(db, target, relation, visitor)
17111731
}),
17121732

1713-
// A `FunctionLiteral` type is a single-valued type like the other literals handled above,
1714-
// so it also, for now, just delegates to its instance fallback.
1715-
(Type::FunctionLiteral(_), _) => KnownClass::FunctionType
1716-
.to_instance(db)
1717-
.has_relation_to_impl(db, target, relation, visitor),
1718-
17191733
// The same reasoning applies for these special callable types:
17201734
(Type::BoundMethod(_), _) => KnownClass::MethodType
17211735
.to_instance(db)
@@ -5979,8 +5993,9 @@ impl<'db> Type<'db> {
59795993
self
59805994
}
59815995
}
5982-
TypeMapping::PromoteLiterals | TypeMapping::BindLegacyTypevars(_) |
5983-
TypeMapping::MarkTypeVarsInferable(_) => self,
5996+
TypeMapping::PromoteLiterals
5997+
| TypeMapping::BindLegacyTypevars(_)
5998+
| TypeMapping::MarkTypeVarsInferable(_) => self,
59845999
TypeMapping::Materialize(materialization_kind) => {
59856000
Type::TypeVar(bound_typevar.materialize_impl(db, *materialization_kind, visitor))
59866001
}
@@ -6000,10 +6015,10 @@ impl<'db> Type<'db> {
60006015
self
60016016
}
60026017
}
6003-
TypeMapping::PromoteLiterals |
6004-
TypeMapping::BindLegacyTypevars(_) |
6005-
TypeMapping::BindSelf(_) |
6006-
TypeMapping::ReplaceSelf { .. }
6018+
TypeMapping::PromoteLiterals
6019+
| TypeMapping::BindLegacyTypevars(_)
6020+
| TypeMapping::BindSelf(_)
6021+
| TypeMapping::ReplaceSelf { .. }
60076022
=> self,
60086023
TypeMapping::Materialize(materialization_kind) => Type::NonInferableTypeVar(bound_typevar.materialize_impl(db, *materialization_kind, visitor))
60096024

@@ -6023,7 +6038,13 @@ impl<'db> Type<'db> {
60236038
}
60246039

60256040
Type::FunctionLiteral(function) => {
6026-
Type::FunctionLiteral(function.with_type_mapping(db, type_mapping))
6041+
let function = Type::FunctionLiteral(function.with_type_mapping(db, type_mapping));
6042+
6043+
match type_mapping {
6044+
TypeMapping::PromoteLiterals => function.literal_promotion_type(db)
6045+
.expect("function literal should have a promotion type"),
6046+
_ => function
6047+
}
60276048
}
60286049

60296050
Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new(
@@ -6129,8 +6150,8 @@ impl<'db> Type<'db> {
61296150
TypeMapping::ReplaceSelf { .. } |
61306151
TypeMapping::MarkTypeVarsInferable(_) |
61316152
TypeMapping::Materialize(_) => self,
6132-
TypeMapping::PromoteLiterals => self.literal_fallback_instance(db)
6133-
.expect("literal type should have fallback instance type"),
6153+
TypeMapping::PromoteLiterals => self.literal_promotion_type(db)
6154+
.expect("literal type should have a promotion type"),
61346155
}
61356156

61366157
Type::Dynamic(_) => match type_mapping {
@@ -6663,8 +6684,8 @@ pub enum TypeMapping<'a, 'db> {
66636684
Specialization(Specialization<'db>),
66646685
/// Applies a partial specialization to the type
66656686
PartialSpecialization(PartialSpecialization<'a, 'db>),
6666-
/// Promotes any literal types to their corresponding instance types (e.g. `Literal["string"]`
6667-
/// to `str`)
6687+
/// Replaces any literal types with their corresponding promoted type form (e.g. `Literal["string"]`
6688+
/// to `str`, or `def _() -> int` to `Callable[[], int]`).
66686689
PromoteLiterals,
66696690
/// Binds a legacy typevar with the generic context (class, function, type alias) that it is
66706691
/// being used in.

0 commit comments

Comments
 (0)