mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
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:
parent
298f43f34e
commit
890ba725d9
5 changed files with 85 additions and 65 deletions
|
@ -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
|
||||
|
|
||||
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
||||
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
||||
|
||||
```
|
||||
|
|
|
@ -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")]
|
||||
```
|
||||
|
|
|
@ -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()])
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue