[red-knot] avoid inferring types if unpacking fails (#16530)

## Summary

This PR closes #15199.

The change I just made is to set all variables to type `Unknown` if
unpacking fails, but in some cases this may be excessive.
For example:

```py
a, b, c = "ab"
reveal_type(a)  # Unknown, but it would be reasonable to think of it as LiteralString
reveal_type(c)  # Unknown
```

```py
# Failed to unpack before the starred expression
(a, b, *c, d, e) = (1,)
reveal_type(a)  # Unknown
reveal_type(b)  # Unknown
...
# Failed to unpack after the starred expression
(a, b, *c, d, e) = (1, 2, 3)
reveal_type(a)  # Unknown, but should it be Literal[1]?
reveal_type(b)  # Unknown, but should it be Literal[2]?
reveal_type(c)  # Todo
reveal_type(d)  # Unknown
reveal_type(e)  # Unknown
```

I will modify it if you think it would be better to make it a different
type than just `Unknown`.

## Test Plan

I have made appropriate modifications to the test cases affected by this
change, and also added some more test cases.
This commit is contained in:
Shunsuke Shibayama 2025-03-08 04:04:44 +09:00 committed by GitHub
parent 6d6e524b90
commit 348c196cb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 69 additions and 36 deletions

View file

@ -1,5 +1,9 @@
# Unpacking
If there are not enough or too many values when unpacking, an error will occur and the types of
all variables (if nested tuple unpacking fails, only the variables within the failed tuples) is
inferred to be `Unknown`.
## Tuple
### Simple tuple
@ -63,8 +67,8 @@ reveal_type(c) # revealed: Literal[4]
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
(a, b, c) = (1, 2)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[2]
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```
@ -73,8 +77,30 @@ reveal_type(c) # revealed: Unknown
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
(a, b) = (1, 2, 3)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
### Nested uneven unpacking (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
(a, (b, c), d) = (1, (2,), 3)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[2]
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Literal[3]
```
### Nested uneven unpacking (2)
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
(a, (b, c), d) = (1, (2, 3, 4), 5)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Literal[5]
```
### Starred expression (1)
@ -82,10 +108,10 @@ reveal_type(b) # revealed: Literal[2]
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
[a, *b, c, d] = (1, 2)
reveal_type(a) # revealed: Literal[1]
reveal_type(a) # revealed: Unknown
# TODO: Should be list[Any] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: Literal[2]
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
```
@ -135,10 +161,10 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 5 or more, got 1)"
(a, b, c, *d, e, f) = (1,)
reveal_type(a) # revealed: Literal[1]
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: @Todo(starred unpacking)
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown
```
@ -201,8 +227,8 @@ reveal_type(b) # revealed: LiteralString
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
a, b, c = "ab"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```
@ -211,8 +237,8 @@ reveal_type(c) # revealed: Unknown
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
a, b = "abc"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
### Starred expression (1)
@ -220,10 +246,19 @@ reveal_type(b) # revealed: LiteralString
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
(a, *b, c, d) = "ab"
reveal_type(a) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: LiteralString
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
```
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 1)"
(a, b, *c, d) = "a"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
```
@ -274,7 +309,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
(a, b) = "é"
reveal_type(a) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
@ -284,7 +319,7 @@ reveal_type(b) # revealed: Unknown
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
(a, b) = "\u9e6c"
reveal_type(a) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
@ -294,7 +329,7 @@ reveal_type(b) # revealed: Unknown
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
(a, b) = "\U0010ffff"
reveal_type(a) # revealed: LiteralString
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
@ -388,8 +423,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 5)"
a, b = arg
reveal_type(a) # revealed: int
reveal_type(b) # revealed: bytes | int
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
### Size mismatch (2)
@ -399,8 +434,8 @@ def _(arg: tuple[int, bytes] | tuple[int, str]):
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
a, b, c = arg
reveal_type(a) # revealed: int
reveal_type(b) # revealed: bytes | str
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```
@ -542,7 +577,7 @@ for a, b in ((1, 2), ("a", "b")):
# error: "Object of type `Literal[4]` is not iterable"
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"):
reveal_type(a) # revealed: Unknown | Literal[3, 5] | LiteralString
reveal_type(a) # revealed: Unknown | Literal[3, 5]
reveal_type(b) # revealed: Unknown | Literal["a", "b"]
```

View file

@ -121,7 +121,7 @@ impl<'db> Unpacker<'db> {
if let Some(tuple_ty) = ty.into_tuple() {
let tuple_ty_elements = self.tuple_ty_elements(target, elts, tuple_ty);
match elts.len().cmp(&tuple_ty_elements.len()) {
let length_mismatch = match elts.len().cmp(&tuple_ty_elements.len()) {
Ordering::Less => {
self.context.report_lint(
&INVALID_ASSIGNMENT,
@ -132,6 +132,7 @@ impl<'db> Unpacker<'db> {
tuple_ty_elements.len()
),
);
true
}
Ordering::Greater => {
self.context.report_lint(
@ -143,13 +144,18 @@ impl<'db> Unpacker<'db> {
tuple_ty_elements.len()
),
);
true
}
Ordering::Equal => {}
}
Ordering::Equal => false,
};
for (index, ty) in tuple_ty_elements.iter().enumerate() {
if let Some(element_types) = target_types.get_mut(index) {
element_types.push(*ty);
if length_mismatch {
element_types.push(Type::unknown());
} else {
element_types.push(*ty);
}
}
}
} else {
@ -243,15 +249,7 @@ impl<'db> Unpacker<'db> {
),
);
let mut element_types = tuple_ty.elements(self.db()).to_vec();
// Subtract 1 to insert the starred expression type at the correct
// index.
element_types.resize(targets.len() - 1, Type::unknown());
// TODO: This should be `list[Unknown]`
element_types.insert(starred_index, todo_type!("starred unpacking"));
Cow::Owned(element_types)
Cow::Owned(vec![Type::unknown(); targets.len()])
}
}