red_knot_python_semantic: migrate INVALID_ASSIGNMENT for unpacking

This moves all INVALID_ASSIGNMENT lints related to unpacking over to the new
diagnostic model.

While we're here, we improve the diagnostic a bit by adding a secondary
annotation covering where the value is. We also split apart the original
singular message into one message for the diagnostic and the "expected
versus got" into annotation messages.
This commit is contained in:
Andrew Gallant 2025-04-16 09:25:50 -04:00 committed by Andrew Gallant
parent 298f43f34e
commit 890ba725d9
5 changed files with 85 additions and 65 deletions

View file

@ -18,11 +18,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Not enough values to unpack
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1,) # error: [invalid-assignment]
| ^^^^ Not enough values to unpack (expected 2, got 1)
| ^^^^ ---- Got 1
| |
| Expected 2
|
```

View file

@ -18,11 +18,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Too many values to unpack
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
| ^^^^ Too many values to unpack (expected 2, got 3)
| ^^^^ --------- Got 3
| |
| Expected 2
|
```

View file

@ -12,17 +12,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack
## mdtest_snippet.py
```
1 | [a, *b, c, d] = (1, 2)
1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Not enough values to unpack
--> /src/mdtest_snippet.py:1:1
|
1 | [a, *b, c, d] = (1, 2)
| ^^^^^^^^^^^^^ Not enough values to unpack (expected 3 or more, got 2)
1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment]
| ^^^^^^^^^^^^^ ------ Got 2
| |
| Expected 3 or more
|
```

View file

@ -65,7 +65,7 @@ reveal_type(c) # revealed: Literal[4]
### Uneven unpacking (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
(a, b, c) = (1, 2)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -75,7 +75,7 @@ reveal_type(c) # revealed: Unknown
### Uneven unpacking (2)
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
(a, b) = (1, 2, 3)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -84,7 +84,7 @@ reveal_type(b) # revealed: Unknown
### Nested uneven unpacking (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
(a, (b, c), d) = (1, (2,), 3)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Unknown
@ -95,7 +95,7 @@ reveal_type(d) # revealed: Literal[3]
### Nested uneven unpacking (2)
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
(a, (b, c), d) = (1, (2, 3, 4), 5)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Unknown
@ -106,7 +106,7 @@ reveal_type(d) # revealed: Literal[5]
### Starred expression (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
[a, *b, c, d] = (1, 2)
reveal_type(a) # revealed: Unknown
# TODO: Should be list[Any] once support for assigning to starred expression is added
@ -159,7 +159,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
### Starred expression (6)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 5 or more, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 5 or more"
(a, b, c, *d, e, f) = (1,)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -225,7 +225,7 @@ reveal_type(b) # revealed: LiteralString
### Uneven unpacking (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
a, b, c = "ab"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -235,7 +235,7 @@ reveal_type(c) # revealed: Unknown
### Uneven unpacking (2)
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
a, b = "abc"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -244,7 +244,7 @@ reveal_type(b) # revealed: Unknown
### Starred expression (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
(a, *b, c, d) = "ab"
reveal_type(a) # revealed: Unknown
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
@ -254,7 +254,7 @@ reveal_type(d) # revealed: Unknown
```
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
(a, b, *c, d) = "a"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -306,7 +306,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
### Unicode
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
(a, b) = "é"
reveal_type(a) # revealed: Unknown
@ -316,7 +316,7 @@ reveal_type(b) # revealed: Unknown
### Unicode escape (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
(a, b) = "\u9e6c"
reveal_type(a) # revealed: Unknown
@ -326,7 +326,7 @@ reveal_type(b) # revealed: Unknown
### Unicode escape (2)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
(a, b) = "\U0010ffff"
reveal_type(a) # revealed: Unknown
@ -420,8 +420,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
```py
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)"
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
a, b = arg
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -431,8 +431,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
```py
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)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
a, b, c = arg
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -575,7 +575,7 @@ for a, b in ((1, 2), ("a", "b")):
# error: "Object of type `Literal[1]` is not iterable"
# error: "Object of type `Literal[2]` is not iterable"
# error: "Object of type `Literal[4]` is not iterable"
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"):
reveal_type(a) # revealed: Unknown | Literal[3, 5]
reveal_type(b) # revealed: Unknown | Literal["a", "b"]
@ -702,7 +702,7 @@ class ContextManager:
def __exit__(self, *args) -> None:
pass
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
with ContextManager() as (a, b, c):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -765,7 +765,7 @@ def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, s
# error: "Object of type `Literal[1]` is not iterable"
# error: "Object of type `Literal[2]` is not iterable"
# error: "Object of type `Literal[4]` is not iterable"
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
# revealed: tuple[Unknown | Literal[3, 5], Unknown | Literal["a", "b"]]
[reveal_type((a, b)) for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c")]
```

View file

@ -134,35 +134,45 @@ impl<'db> Unpacker<'db> {
};
if let Some(tuple_ty) = ty.into_tuple() {
let tuple_ty_elements = self.tuple_ty_elements(target, elts, tuple_ty);
let tuple_ty_elements =
self.tuple_ty_elements(target, elts, tuple_ty, value_expr);
let length_mismatch = match elts.len().cmp(&tuple_ty_elements.len()) {
Ordering::Less => {
self.context.report_lint_old(
&INVALID_ASSIGNMENT,
target,
format_args!(
"Too many values to unpack (expected {}, got {})",
elts.len(),
tuple_ty_elements.len()
),
);
true
}
Ordering::Greater => {
self.context.report_lint_old(
&INVALID_ASSIGNMENT,
target,
format_args!(
"Not enough values to unpack (expected {}, got {})",
elts.len(),
tuple_ty_elements.len()
),
);
true
}
Ordering::Equal => false,
};
let length_mismatch =
match elts.len().cmp(&tuple_ty_elements.len()) {
Ordering::Less => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Too many values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(self.context.secondary(value_expr).message(
format_args!("Got {}", tuple_ty_elements.len()),
));
}
true
}
Ordering::Greater => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Not enough values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(self.context.secondary(value_expr).message(
format_args!("Got {}", tuple_ty_elements.len()),
));
}
true
}
Ordering::Equal => false,
};
for (index, ty) in tuple_ty_elements.iter().enumerate() {
if let Some(element_types) = target_types.get_mut(index) {
@ -203,11 +213,15 @@ impl<'db> Unpacker<'db> {
/// Returns the [`Type`] elements inside the given [`TupleType`] taking into account that there
/// can be a starred expression in the `elements`.
///
/// `value_expr` is an AST reference to the value being unpacked. It is
/// only used for diagnostics.
fn tuple_ty_elements(
&self,
expr: &ast::Expr,
targets: &[ast::Expr],
tuple_ty: TupleType<'db>,
value_expr: AnyNodeRef<'_>,
) -> Cow<'_, [Type<'db>]> {
// If there is a starred expression, it will consume all of the types at that location.
let Some(starred_index) = targets.iter().position(ast::Expr::is_starred_expr) else {
@ -254,15 +268,15 @@ impl<'db> Unpacker<'db> {
Cow::Owned(element_types)
} else {
self.context.report_lint_old(
&INVALID_ASSIGNMENT,
expr,
format_args!(
"Not enough values to unpack (expected {} or more, got {})",
targets.len() - 1,
tuple_ty.len(self.db())
),
);
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, expr) {
let mut diag = builder.into_diagnostic("Not enough values to unpack");
diag.set_primary_message(format_args!("Expected {} or more", targets.len() - 1));
diag.annotate(
self.context
.secondary(value_expr)
.message(format_args!("Got {}", tuple_ty.len(self.db()))),
);
}
Cow::Owned(vec![Type::unknown(); targets.len()])
}