From 8d98c601d8fb0c7fafe503c6fa22427357eb5080 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 3 Jun 2025 19:17:47 +0530 Subject: [PATCH] [ty] Infer `list[T]` when unpacking non-tuple type (#18438) ## Summary Follow-up from #18401, I was looking at whether that would fix the issue at https://github.com/astral-sh/ty/issues/247#issuecomment-2917656676 and it didn't, which made me realize that the PR only inferred `list[T]` when the value type was tuple but it could be other types as well. This PR fixes the actual issue by inferring `list[T]` for the non-tuple type case. ## Test Plan Add test cases for starred expression involved with non-tuple type. I also added a few test cases for list type and list literal. I also verified that the example in the linked issue comment works: ```py def _(line: str): a, b, *c = line.split(maxsplit=2) c.pop() ``` --- .../resources/mdtest/unpacking.md | 69 ++++++++++++++++++- .../ty_python_semantic/src/types/unpacker.rs | 11 ++- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/unpacking.md b/crates/ty_python_semantic/resources/mdtest/unpacking.md index 93d5c314f6..ac94dd99fe 100644 --- a/crates/ty_python_semantic/resources/mdtest/unpacking.md +++ b/crates/ty_python_semantic/resources/mdtest/unpacking.md @@ -1,8 +1,8 @@ # 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`. +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 @@ -207,6 +207,57 @@ reveal_type(c) # revealed: int reveal_type(d) # revealed: Literal[2] ``` +## List + +### Literal unpacking + +```py +a, b = [1, 2] +# TODO: should be `int` for both `a` and `b` +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +``` + +### Simple unpacking + +```py +def _(value: list[int]): + a, b = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int +``` + +### Nested unpacking + +```py +def _(value: list[list[int]]): + a, (b, c) = value + reveal_type(a) # revealed: list[int] + reveal_type(b) # revealed: int + reveal_type(c) # revealed: int +``` + +### Invalid nested unpacking + +```py +def _(value: list[int]): + # error: [not-iterable] "Object of type `int` is not iterable" + a, (b, c) = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown +``` + +### Starred expression + +```py +def _(value: list[int]): + a, *b, c = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: list[int] + reveal_type(c) # revealed: int +``` + ## String ### Simple unpacking @@ -293,6 +344,18 @@ reveal_type(b) # revealed: LiteralString reveal_type(c) # revealed: list[LiteralString] ``` +### Starred expression (6) + +```py +from typing_extensions import LiteralString + +def _(s: LiteralString): + a, b, *c = s + reveal_type(a) # revealed: LiteralString + reveal_type(b) # revealed: LiteralString + reveal_type(c) # revealed: list[LiteralString] +``` + ### Unicode ```py diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index d790a872dd..07c10ce683 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -192,8 +192,15 @@ impl<'db> Unpacker<'db> { err.fallback_element_type(self.db()) }) }; - for target_type in &mut target_types { - target_type.push(ty); + // Both `elts` and `target_types` are guaranteed to have the same length. + for (element, target_type) in elts.iter().zip(&mut target_types) { + if element.is_starred_expr() { + target_type.push( + KnownClass::List.to_specialized_instance(self.db(), [ty]), + ); + } else { + target_type.push(ty); + } } } }