Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] Add diagnostic for invalid unpacking #15086

Merged
merged 1 commit into from
Dec 30, 2024

Conversation

dhruvmanila
Copy link
Member

@dhruvmanila dhruvmanila commented Dec 20, 2024

Summary

Part of #13773

This PR adds diagnostics when there is a length mismatch during unpacking between the number of target expressions and the number of types for the unpack value expression.

There are 3 cases of diagnostics here where the first two occurs when there isn't a starred expression and the last one occurs when there's a starred expression:

  1. Number of target expressions is less than the number of types that needs to be unpacked
  2. Number of target expressions is greater then the number of types that needs to be unpacked
  3. When there's a starred expression as one of the target expression and the number of target expressions is greater than the number of types

Examples for all each of the above cases:

# red-knot: Too many values to unpack (expected 2, got 3) [lint:invalid-assignment]
a, b = (1, 2, 3)

# red-knot: Not enough values to unpack (expected 2, got 1) [lint:invalid-assignment]
a, b = (1,)

# red-knot: Not enough values to unpack (expected 3 or more, got 2) [lint:invalid-assignment]
a, *b, c, d = (1, 2)

The (3) case is a bit special because it uses a distinct wording "expected n or more" instead of "expected n" because of the starred expression.

Location

The diagnostic location is the target expression that's being unpacked. For nested targets, the location will be the nested expression. For example:

(a, (b, c), d) = (1, (2, 3, 4), 5)
#   ^^^^^^
#   red-knot: Too many values to unpack (expected 2, got 3) [lint:invalid-assignment]

For future improvements, it would be useful to show the context for why this unpacking failed. For example, for why the expected number of targets is n, we can highlight the relevant elements for the value expression.

In the ecosystem, Pyright uses the target expressions for location while mypy uses the value expression for the location. For example:

if 1:
#          mypy: Too many values to unpack (2 expected, 3 provided)  [misc]
#          vvvvvvvvv
	a, b = (1, 2, 3)
#   ^^^^
#   Pyright: Expression with type "tuple[Literal[1], Literal[2], Literal[3]]" cannot be assigned to target tuple
#     Type "tuple[Literal[1], Literal[2], Literal[3]]" is incompatible with target tuple
#       Tuple size mismatch; expected 2 but received 3 [reportAssignmentType]
#   red-knot: Too many values to unpack (expected 2, got 3) [lint:invalid-assignment]

Test Plan

Update existing test cases TODO with the error directives.

@dhruvmanila dhruvmanila added the red-knot Multi-file analysis & type inference label Dec 20, 2024
Copy link
Contributor

github-actions bot commented Dec 20, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@dhruvmanila dhruvmanila force-pushed the dhruv/unpack-for-target branch from fe681d0 to 6a116dc Compare December 23, 2024 06:09
Base automatically changed from dhruv/unpack-for-target to main December 23, 2024 06:13
@dhruvmanila dhruvmanila force-pushed the dhruv/unpack-diagnostic branch 3 times, most recently from f0b0385 to ff53cb1 Compare December 24, 2024 09:47
@dhruvmanila dhruvmanila marked this pull request as ready for review December 24, 2024 09:58
Comment on lines 170 to 173
fn tuple_ty_elements(
&mut self,
&self,
expr: &ast::Expr,
targets: &[ast::Expr],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional changes:

  • Expanded the comments in this method
  • Avoid &mut self as it's not required (not sure why clippy didn't catch that)
  • Pass the expr which is required for the diagnostic location

Comment on lines 541 to 543
# error: "Not enough values to unpack (expected 2, got 1)"
for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"):
reveal_type(a) # revealed: Unknown | Literal[3, 5] | LiteralString
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-commenttng here,
Since "c" doesn't have enough values to unpack, I think LiteralString will never put at here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it would be better if this were just Unknown | Literal[3, 5].

Copy link
Contributor

@carljm carljm Dec 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think to do this consistently would mean changing a bunch of other cases, though -- it would mean that e.g. for (a, b, c) = (1, 2) we would infer Unknown (or Never would also be reasonable) for all of a, b, c, rather than inferring Literal[1] for a and Literal[2] for b. That is, today we try to match up and infer as many types as we can, even if the overall unpacking is an error; this would mean changing that.

The advantage of the current approach is that it can catch more errors at once, assuming the parts that we are able to line up are actually lined up correctly. But that may not be a reasonable assumption; if you have a mis-alignment it could actually be early in the unpacking, in which case we are just inferring wrong types for many of the targets, potentially leading to false positives in subsequent code.

It looks like our current behavior is more like pyright; mypy always just infers Any for all targets in a mis-aligned unpacking.

I do think that the mypy behavior is more principled and we should probably switch to that, though I also don't think it's high priority to make that change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I did that for the advantage that you've mentioned and I'm fine changing the behavior as it's an error case and should be less often in practice. I'll expand the original issue with this task.

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice!

Comment on lines 541 to 543
# error: "Not enough values to unpack (expected 2, got 1)"
for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"):
reveal_type(a) # revealed: Unknown | Literal[3, 5] | LiteralString
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it would be better if this were just Unknown | Literal[3, 5].

@dhruvmanila dhruvmanila force-pushed the dhruv/unpack-diagnostic branch from ff53cb1 to 9fd4eb8 Compare December 30, 2024 07:35
@dhruvmanila dhruvmanila merged commit 8a98d88 into main Dec 30, 2024
21 checks passed
@dhruvmanila dhruvmanila deleted the dhruv/unpack-diagnostic branch December 30, 2024 07:40
dcreager added a commit that referenced this pull request Dec 30, 2024
* main:
  Add all PEP-585 names to UP006 rule (#5454)
  [`flake8-simplify`] More precise inference for dictionaries (`SIM300`) (#15164)
  `@no_type_check` support (#15122)
  Visit PEP 764 inline `TypedDict`s' keys as non-type-expressions (#15073)
  [red-knot] Add diagnostic for invalid unpacking (#15086)
  [`flake8-use-pathlib`] Catch redundant joins in `PTH201` and avoid syntax errors (#15177)
  Update Rust crate glob to v0.3.2 (#15185)
  Update astral-sh/setup-uv action to v5 (#15193)
  Update dependency mdformat-mkdocs to v4.1.1 (#15192)
  Update Rust crate serde_with to v3.12.0 (#15191)
  Update NPM Development dependencies (#15190)
  Update pre-commit hook rhysd/actionlint to v1.7.5 (#15189)
  Update Rust crate syn to v2.0.93 (#15188)
  Update Rust crate serde to v1.0.217 (#15187)
  Update Rust crate quote to v1.0.38 (#15186)
  Update Rust crate compact_str to v0.8.1 (#15184)
  [`flake8-type-checking`] Disable TC006 & TC007 in stub files (#15179)
  Test explicit shadowing involving `def`s (#15174)
  Fix typo in `NameImport.qualified_name` docstring (#15170)
  [airflow]: extend names moved from core to provider (AIR303) (#15159)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
red-knot Multi-file analysis & type inference
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants