Skip to content

Commit f0b0385

Browse files
committed
[red-knot] Add diagnostic for invalid unpacking
1 parent 2835d94 commit f0b0385

File tree

3 files changed

+89
-29
lines changed

3 files changed

+89
-29
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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "Too many values to unpack (expected 2, got 3)"
387+
# error: "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: "Not enough values to unpack (expected 3, got 2)"
398+
# error: "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: "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: 68 additions & 17 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,46 @@ 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, has_starred_expression) =
110+
self.tuple_ty_elements(elts, tuple_ty);
108111

109-
// TODO: Add diagnostic for length mismatch
112+
match elts.len().cmp(&tuple_ty_elements.len()) {
113+
Ordering::Less => {
114+
self.context.report_lint(
115+
&INVALID_ASSIGNMENT,
116+
target.into(),
117+
format_args!(
118+
"Too many values to unpack (expected {}, got {})",
119+
elts.len(),
120+
tuple_ty_elements.len()
121+
),
122+
);
123+
}
124+
Ordering::Greater => {
125+
if has_starred_expression {
126+
self.context.report_lint(
127+
&INVALID_ASSIGNMENT,
128+
target.into(),
129+
format_args!(
130+
"Not enough values to unpack (expected {} or more, got {})",
131+
elts.len() - 1,
132+
tuple_ty.len(self.db())
133+
),
134+
);
135+
} else {
136+
self.context.report_lint(
137+
&INVALID_ASSIGNMENT,
138+
target.into(),
139+
format_args!(
140+
"Not enough values to unpack (expected {}, got {})",
141+
elts.len(),
142+
tuple_ty_elements.len()
143+
),
144+
);
145+
}
146+
}
147+
Ordering::Equal => {}
148+
}
110149

111150
for (index, ty) in tuple_ty_elements.iter().enumerate() {
112151
if let Some(element_types) = target_types.get_mut(index) {
@@ -129,6 +168,7 @@ impl<'db> Unpacker<'db> {
129168
for (index, element) in elts.iter().enumerate() {
130169
// SAFETY: `target_types` is initialized with the same length as `elts`.
131170
let element_ty = match target_types[index].as_slice() {
171+
[] if element.is_starred_expr() => todo_type!("starred unpacking"),
132172
[] => Type::Unknown,
133173
types => UnionType::from_elements(self.db(), types),
134174
};
@@ -142,29 +182,39 @@ impl<'db> Unpacker<'db> {
142182
/// Returns the [`Type`] elements inside the given [`TupleType`] taking into account that there
143183
/// can be a starred expression in the `elements`.
144184
fn tuple_ty_elements(
145-
&mut self,
185+
&self,
146186
targets: &[ast::Expr],
147187
tuple_ty: TupleType<'db>,
148-
) -> Cow<'_, [Type<'db>]> {
149-
// If there is a starred expression, it will consume all of the entries at that location.
188+
) -> (Cow<'_, [Type<'db>]>, bool) {
189+
// If there is a starred expression, it will consume all of the types at that location.
150190
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.
152-
return Cow::Borrowed(tuple_ty.elements(self.db()).as_ref());
191+
// Otherwise, the types will be unpacked 1-1 to the targets.
192+
return (Cow::Borrowed(tuple_ty.elements(self.db()).as_ref()), false);
153193
};
154194

155195
if tuple_ty.len(self.db()) >= targets.len() - 1 {
196+
// This branch is only taken when there are enough elements in the tuple type to
197+
// combine for the starred expression. So, the arithmetic and indexing operations are
198+
// safe to perform.
156199
let mut element_types = Vec::with_capacity(targets.len());
200+
201+
// Insert all the elements before the starred expression.
157202
element_types.extend_from_slice(
158203
// SAFETY: Safe because of the length check above.
159204
&tuple_ty.elements(self.db())[..starred_index],
160205
);
161206

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.
207+
// The number of target expressions that are remaining after the starred expression.
208+
// For example, in `(a, *b, c, d) = ...`, the index of starred element `b` is 1 and the
209+
// remaining elements after that are 2.
164210
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.
211+
212+
// This index represents the position of the last element that belongs to the starred
213+
// expression, in an exclusive manner. For example, in `(a, *b, c) = (1, 2, 3, 4)`, the
214+
// starred expression `b` will consume the elements `Literal[2]` and `Literal[3]` and
215+
// the index value would be 3.
167216
let starred_end_index = tuple_ty.len(self.db()) - remaining;
217+
168218
// SAFETY: Safe because of the length check above.
169219
let _starred_element_types =
170220
&tuple_ty.elements(self.db())[starred_index..starred_end_index];
@@ -173,19 +223,20 @@ impl<'db> Unpacker<'db> {
173223
// combine_types(starred_element_types);
174224
element_types.push(todo_type!("starred unpacking"));
175225

226+
// Insert the types remaining that aren't consumed by the starred expression.
176227
element_types.extend_from_slice(
177228
// SAFETY: Safe because of the length check above.
178229
&tuple_ty.elements(self.db())[starred_end_index..],
179230
);
180-
Cow::Owned(element_types)
181-
} else {
231+
232+
(Cow::Owned(element_types), true)
233+
} else if starred_index < tuple_ty.len(self.db()) {
182234
let mut element_types = tuple_ty.elements(self.db()).to_vec();
183-
// Subtract 1 to insert the starred expression type at the correct
184-
// index.
185-
element_types.resize(targets.len() - 1, Type::Unknown);
186235
// TODO: This should be `list[Unknown]`
187236
element_types.insert(starred_index, todo_type!("starred unpacking"));
188-
Cow::Owned(element_types)
237+
(Cow::Owned(element_types), true)
238+
} else {
239+
(Cow::Borrowed(tuple_ty.elements(self.db())), true)
189240
}
190241
}
191242

0 commit comments

Comments
 (0)