Fix false-positive unused-variable on unpacked loop variables#3941
Open
mikeleppane wants to merge 1 commit into
Open
Fix false-positive unused-variable on unpacked loop variables#3941mikeleppane wants to merge 1 commit into
mikeleppane wants to merge 1 commit into
Conversation
The unused-variable check only registered single-name `x = ...` assignments. Tuple/list-unpacking, multi-target, `for`, and `with` targets all flow through `bind_target_name`, which never registered them. So a name first bound by unpacking (e.g. `li, ri = 0, 100`) was absent from the usage map: the loop-header reads couldn't mark it used, and a later single-name reassignment inside the loop registered it fresh as unused. The variable was wrongly reported even though it is read every iteration via the loop back-edge. Register unpacking/multi-target/`for`/`with` targets too, but as tracking-only (`allow_unused`) so they are never themselves reported, preserving today's policy that only single-name assignments are flagged. The tracking-only registration is insert-if-absent so it cannot overwrite and suppress a real single-name report; a later single-name assignment still overwrites and stays reportable. Fixes facebook#3911
|
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3911
What
A loop variable first bound by tuple unpacking and then reassigned inside the loop was reported as unused, even though it is read on every iteration:
The reporter saw it only with a
Generatorreturn type, but that was a coincidence. It reproduces with any return type, and with no read after the reassignment bothliandriget flagged.Why
unused-variableis a binding-phase check: each assignment registers the name in a per-scope usage map, each read marks it used, and names still unused at scope exit are reported.mark_variable_usedis a no-op if the name is not already in the map, so a name has to be registered before its reads to be tracked.Only single-name assignments (
x = ...) registered. Tuple/list unpacking, multi-target (a = b = ...),for, andwithtargets all flow throughbind_target_name, which never calledregister_variable. Soli, ri = 0, 100left both names out of the map. The loop-header reads (while li <= ri,mid = (li + ri)) found nothing to mark, and the later single-name writeri = mid - 1registeredrifresh as unused. With no read after that point in source order, it was reported.lisurvived only becauseyield lireads it after its own reassignment.How
bind_target_namenow registers its target, but as tracking-only (allow_unused = true). The name is tracked so its reads land and the read-before-reassignment carry works across loop iterations, but it is never itself reported. This keeps the existing policy that only single-name assignments are flagged.x = 1; x, y = f()still reportsx. A later single-name assignment is not tracking-only and still overwrites, staying reportable.VariableUsagegains anallow_unusedfield, matchingParameterUsage.allow_unusedandImportUsage.skip_unused_check, which already had equivalent flags.for,with,_, or comprehension reporting. Comprehension targets reachbind_target_namebut are inert, because the tracker only coversModule,Function, andMethodscopes.Files:
binding/scope.rs,binding/target.rs,binding/bindings.rs, plus tests.Test plan
All tests use the
get_unused_variable_diagnosticsharness intest/lsp/diagnostic.rs.test_unused_variable_unpacked_reassigned_in_loopGenerator) reports nothingtest_unused_variable_unpacked_reassigned_in_loop_no_trailing_readliandri) reports nothingtest_unused_variable_single_then_unpack_still_reportedx = 1; x, y = (2,3)still reportsx(no-clobber guard)test_unused_variable_unpacking_target_not_reporteda, b = 1, 2; print(a)stays unflaggedtest_unused_variable_for_loop_target_not_reportedfor i in range(3)stays unflaggedtest_unused_variable_with_target_not_reportedwith cm as xstays unflaggedtest_unused_variable_walrus_target_not_reported(x := 5)stays unflaggedtest_unused_variable_multi_target_not_reporteda = b = 5stays unflaggedtest_unused_variable_comprehension_target_not_reported[1 for x in items]stays unflaggedtest_unused_variable_comprehension_walrus_not_reportedFull lib suite: 5869 passed, 0 failed. Format and lint clean. A wide
mypy_primerrun over 73 projects showed zero diff, which is expected:unused-variableis an IDE-only hint and is never emitted bypyrefly check, so it does not appear in primer output.Future improvements
This PR fixes the false positive without widening what gets reported. The check still only flags plain single-name local assignments. The cases below are deliberately left uncovered and could be follow-up PRs. Most of them need a dummy-name (
_) exemption first, since pyrefly has none today and reporting these without one would flag_everywhere.a, b = f(); print(a)does not flagb.fortargets:for i in range(3): passdoes not flagi.with ... astargets:with cm as x: passdoes not flagx.(x := 5)does not flagx.a = b = 5; print(a)does not flagb.[1 for x in items]does not flagx. These live in a separate scope the tracker does not cover, so adding them is a larger change than the others.x = 5; print(x); x = 7does not flag the finalx = 7, whose value is never read. This is tracked by the existingtest_reassignment_false_negative, which carries a TODO.A reasonable sequencing for the follow-up work: add the
_/ dummy-name exemption, then decide per construct whether to report it (assignment unpacking is the least noisy candidate,forindices the most noisy), and runmypy_primerfor each since these would change reported diagnostics. Theallow_unusedflag added here is the seam where that policy would be flipped per call site.