Skip to content

Commit 9fd4eb8

Browse files
committed
[red-knot] Add diagnostic for invalid unpacking
1 parent 901b7dd commit 9fd4eb8

File tree

3 files changed

+81
-21
lines changed

3 files changed

+81
-21
lines changed

crates/red_knot_python_semantic/resources/mdtest/unpacking.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ reveal_type(c) # revealed: Literal[4]
6161
### Uneven unpacking (1)
6262

6363
```py
64-
# TODO: Add diagnostic (there aren't enough values to unpack)
64+
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
6565
(a, b, c) = (1, 2)
6666
reveal_type(a) # revealed: Literal[1]
6767
reveal_type(b) # revealed: Literal[2]
@@ -71,7 +71,7 @@ reveal_type(c) # revealed: Unknown
7171
### Uneven unpacking (2)
7272

7373
```py
74-
# TODO: Add diagnostic (too many values to unpack)
74+
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
7575
(a, b) = (1, 2, 3)
7676
reveal_type(a) # revealed: Literal[1]
7777
reveal_type(b) # revealed: Literal[2]
@@ -80,7 +80,7 @@ reveal_type(b) # revealed: Literal[2]
8080
### Starred expression (1)
8181

8282
```py
83-
# TODO: Add diagnostic (need more values to unpack)
83+
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
8484
[a, *b, c, d] = (1, 2)
8585
reveal_type(a) # revealed: Literal[1]
8686
# TODO: Should be list[Any] once support for assigning to starred expression is added
@@ -133,7 +133,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
133133
### Starred expression (6)
134134

135135
```py
136-
# TODO: Add diagnostic (need more values to unpack)
136+
# error: [invalid-assignment] "Not enough values to unpack (expected 5 or more, got 1)"
137137
(a, b, c, *d, e, f) = (1,)
138138
reveal_type(a) # revealed: Literal[1]
139139
reveal_type(b) # revealed: Unknown
@@ -199,7 +199,7 @@ reveal_type(b) # revealed: LiteralString
199199
### Uneven unpacking (1)
200200

201201
```py
202-
# TODO: Add diagnostic (there aren't enough values to unpack)
202+
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
203203
a, b, c = "ab"
204204
reveal_type(a) # revealed: LiteralString
205205
reveal_type(b) # revealed: LiteralString
@@ -209,7 +209,7 @@ reveal_type(c) # revealed: Unknown
209209
### Uneven unpacking (2)
210210

211211
```py
212-
# TODO: Add diagnostic (too many values to unpack)
212+
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
213213
a, b = "abc"
214214
reveal_type(a) # revealed: LiteralString
215215
reveal_type(b) # revealed: LiteralString
@@ -218,7 +218,7 @@ reveal_type(b) # revealed: LiteralString
218218
### Starred expression (1)
219219

220220
```py
221-
# TODO: Add diagnostic (need more values to unpack)
221+
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
222222
(a, *b, c, d) = "ab"
223223
reveal_type(a) # revealed: LiteralString
224224
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
@@ -271,7 +271,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
271271
### Unicode
272272

273273
```py
274-
# TODO: Add diagnostic (need more values to unpack)
274+
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
275275
(a, b) = "é"
276276

277277
reveal_type(a) # revealed: LiteralString
@@ -281,7 +281,7 @@ reveal_type(b) # revealed: Unknown
281281
### Unicode escape (1)
282282

283283
```py
284-
# TODO: Add diagnostic (need more values to unpack)
284+
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
285285
(a, b) = "\u9E6C"
286286

287287
reveal_type(a) # revealed: LiteralString
@@ -291,7 +291,7 @@ reveal_type(b) # revealed: Unknown
291291
### Unicode escape (2)
292292

293293
```py
294-
# TODO: Add diagnostic (need more values to unpack)
294+
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
295295
(a, b) = "\U0010FFFF"
296296

297297
reveal_type(a) # revealed: LiteralString
@@ -383,7 +383,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
383383

384384
```py
385385
def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
386-
# TODO: Add diagnostic (too many values to unpack)
386+
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
387+
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 5)"
387388
a, b = arg
388389
reveal_type(a) # revealed: int
389390
reveal_type(b) # revealed: bytes | int
@@ -393,7 +394,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
393394

394395
```py
395396
def _(arg: tuple[int, bytes] | tuple[int, str]):
396-
# TODO: Add diagnostic (there aren't enough values to unpack)
397+
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
398+
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
397399
a, b, c = arg
398400
reveal_type(a) # revealed: int
399401
reveal_type(b) # revealed: bytes | str
@@ -536,6 +538,7 @@ for a, b in ((1, 2), ("a", "b")):
536538
# error: "Object of type `Literal[1]` is not iterable"
537539
# error: "Object of type `Literal[2]` is not iterable"
538540
# error: "Object of type `Literal[4]` is not iterable"
541+
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
539542
for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"):
540543
reveal_type(a) # revealed: Unknown | Literal[3, 5] | LiteralString
541544
reveal_type(b) # revealed: Unknown | Literal["a", "b"]

crates/red_knot_python_semantic/src/types/display.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,12 @@ impl<'db> TypeArrayDisplay<'db> for Vec<Type<'db>> {
324324
}
325325
}
326326

327+
impl<'db> TypeArrayDisplay<'db> for [Type<'db>] {
328+
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray {
329+
DisplayTypeArray { types: self, db }
330+
}
331+
}
332+
327333
pub(crate) struct DisplayTypeArray<'b, 'db> {
328334
types: &'b [Type<'db>],
329335
db: &'db dyn Db,

crates/red_knot_python_semantic/src/types/unpacker.rs

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::borrow::Cow;
2+
use std::cmp::Ordering;
23

34
use rustc_hash::FxHashMap;
45

@@ -11,6 +12,7 @@ use crate::unpack::UnpackValue;
1112
use crate::Db;
1213

1314
use super::context::{InferContext, WithDiagnostics};
15+
use super::diagnostic::INVALID_ASSIGNMENT;
1416
use super::{TupleType, UnionType};
1517

1618
/// Unpacks the value expression type to their respective targets.
@@ -104,9 +106,33 @@ impl<'db> Unpacker<'db> {
104106
};
105107

106108
if let Some(tuple_ty) = ty.into_tuple() {
107-
let tuple_ty_elements = self.tuple_ty_elements(elts, tuple_ty);
109+
let tuple_ty_elements = self.tuple_ty_elements(target, elts, tuple_ty);
108110

109-
// TODO: Add diagnostic for length mismatch
111+
match elts.len().cmp(&tuple_ty_elements.len()) {
112+
Ordering::Less => {
113+
self.context.report_lint(
114+
&INVALID_ASSIGNMENT,
115+
target.into(),
116+
format_args!(
117+
"Too many values to unpack (expected {}, got {})",
118+
elts.len(),
119+
tuple_ty_elements.len()
120+
),
121+
);
122+
}
123+
Ordering::Greater => {
124+
self.context.report_lint(
125+
&INVALID_ASSIGNMENT,
126+
target.into(),
127+
format_args!(
128+
"Not enough values to unpack (expected {}, got {})",
129+
elts.len(),
130+
tuple_ty_elements.len()
131+
),
132+
);
133+
}
134+
Ordering::Equal => {}
135+
}
110136

111137
for (index, ty) in tuple_ty_elements.iter().enumerate() {
112138
if let Some(element_types) = target_types.get_mut(index) {
@@ -142,29 +168,40 @@ impl<'db> Unpacker<'db> {
142168
/// Returns the [`Type`] elements inside the given [`TupleType`] taking into account that there
143169
/// can be a starred expression in the `elements`.
144170
fn tuple_ty_elements(
145-
&mut self,
171+
&self,
172+
expr: &ast::Expr,
146173
targets: &[ast::Expr],
147174
tuple_ty: TupleType<'db>,
148175
) -> Cow<'_, [Type<'db>]> {
149-
// If there is a starred expression, it will consume all of the entries at that location.
176+
// If there is a starred expression, it will consume all of the types at that location.
150177
let Some(starred_index) = targets.iter().position(ast::Expr::is_starred_expr) else {
151-
// Otherwise, the types will be unpacked 1-1 to the elements.
178+
// Otherwise, the types will be unpacked 1-1 to the targets.
152179
return Cow::Borrowed(tuple_ty.elements(self.db()).as_ref());
153180
};
154181

155182
if tuple_ty.len(self.db()) >= targets.len() - 1 {
183+
// This branch is only taken when there are enough elements in the tuple type to
184+
// combine for the starred expression. So, the arithmetic and indexing operations are
185+
// safe to perform.
156186
let mut element_types = Vec::with_capacity(targets.len());
187+
188+
// Insert all the elements before the starred expression.
157189
element_types.extend_from_slice(
158190
// SAFETY: Safe because of the length check above.
159191
&tuple_ty.elements(self.db())[..starred_index],
160192
);
161193

162-
// E.g., in `(a, *b, c, d) = ...`, the index of starred element `b`
163-
// is 1 and the remaining elements after that are 2.
194+
// The number of target expressions that are remaining after the starred expression.
195+
// For example, in `(a, *b, c, d) = ...`, the index of starred element `b` is 1 and the
196+
// remaining elements after that are 2.
164197
let remaining = targets.len() - (starred_index + 1);
165-
// This index represents the type of the last element that belongs
166-
// to the starred expression, in an exclusive manner.
198+
199+
// This index represents the position of the last element that belongs to the starred
200+
// expression, in an exclusive manner. For example, in `(a, *b, c) = (1, 2, 3, 4)`, the
201+
// starred expression `b` will consume the elements `Literal[2]` and `Literal[3]` and
202+
// the index value would be 3.
167203
let starred_end_index = tuple_ty.len(self.db()) - remaining;
204+
168205
// SAFETY: Safe because of the length check above.
169206
let _starred_element_types =
170207
&tuple_ty.elements(self.db())[starred_index..starred_end_index];
@@ -173,18 +210,32 @@ impl<'db> Unpacker<'db> {
173210
// combine_types(starred_element_types);
174211
element_types.push(todo_type!("starred unpacking"));
175212

213+
// Insert the types remaining that aren't consumed by the starred expression.
176214
element_types.extend_from_slice(
177215
// SAFETY: Safe because of the length check above.
178216
&tuple_ty.elements(self.db())[starred_end_index..],
179217
);
218+
180219
Cow::Owned(element_types)
181220
} else {
221+
self.context.report_lint(
222+
&INVALID_ASSIGNMENT,
223+
expr.into(),
224+
format_args!(
225+
"Not enough values to unpack (expected {} or more, got {})",
226+
targets.len() - 1,
227+
tuple_ty.len(self.db())
228+
),
229+
);
230+
182231
let mut element_types = tuple_ty.elements(self.db()).to_vec();
232+
183233
// Subtract 1 to insert the starred expression type at the correct
184234
// index.
185235
element_types.resize(targets.len() - 1, Type::Unknown);
186236
// TODO: This should be `list[Unknown]`
187237
element_types.insert(starred_index, todo_type!("starred unpacking"));
238+
188239
Cow::Owned(element_types)
189240
}
190241
}

0 commit comments

Comments
 (0)