Skip to content

fix: don't flag names as unused when read before their registering assignment (#3911)#4074

Open
AhmedHedi0 wants to merge 1 commit into
facebook:mainfrom
AhmedHedi0:fix/3911-unused-variable
Open

fix: don't flag names as unused when read before their registering assignment (#3911)#4074
AhmedHedi0 wants to merge 1 commit into
facebook:mainfrom
AhmedHedi0:fix/3911-unused-variable

Conversation

@AhmedHedi0

@AhmedHedi0 AhmedHedi0 commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Fixes #3911

Unused-variable tracking only registers a name in register_variable at the
moment a single-name assignment binds it. Reads of a name encountered
before its only registering assignment e.g. a while loop condition or
body reading a name that's only ever assigned inside the loop, or a name
that's only ever assigned via tuple/list unpacking, for, with, or walrus
targets (none of which call register_variable) were silently dropped by
mark_variable_used, since it only marked names already present in
variables. That produced the false-positive "unused variable" diagnostic in
#3911, where a while loop reads and reassigns two names originally bound
via tuple unpacking.

The fix gives mark_variable_used its own scope-resolution walk, mirroring
look_up_name_for_read's logic (skip enclosing class scopes from nested code
blocks, skip walrus targets not yet visible in a comprehension's flow, skip
global/nonlocal captures), but keyed off static scope info rather than
flow, since static scope already knows every name a scope will eventually
bind. When a read resolves to a name that's statically local to a
module/function/method scope but not yet registered, it's recorded in a new
used_variables_before_registration set, which register_variable checks
(alongside the existing used flag) when seeding a newly registered
variable's initial used state. This closes the gap for reads occurring
before their name's only registering assignment, without changing behavior
for the common case where a read follows registration.

Note: I noticed the issue already has an assignee with what may be an
in-progress PR. Opening this in case the approach here is useful for
comparison, happy to defer to whichever fix maintainers prefer, or to have
this closed if it's redundant.

AI disclosure: this PR was developed with AI assistance under my direction
and review. I wrote none of the diff by hand, but reviewed the root-cause
analysis, caught and had it fix a regression in an early version(false-positive
unused-variable when a name is shadowed by an enclosingclass's field),
and verified the fix and test results myself before opening this PR.

Test Plan

  • Added regression tests in pyrefly/lib/test/lsp/diagnostic.rs covering the
    original repro (test_generator_loop_reassignments), the same pattern
    without a generator to confirm the bug isn't generator-specific
    (test_unpacked_target_reassigned_in_loop), plus for/with/walrus/
    comprehension/nonlocal cases exercising the same read-before-registration
    code path.
  • Added test_method_read_of_name_shadowed_by_class_field to guard against a
    regression caught during review, where an early version of the fix broke
    name resolution across class-body boundaries.
  • Ran python3 test.py in full (fmt, lint, tests, conformance, jsonschema).
    All pass except 16 pre-existing test::shape_dsl::* timeout failures
    ("Test took too long (> 20s)"), which I confirmed are unrelated to this
    change by reproducing the identical failure set on unmodified main.
  • No generated files (conformance/jsonschema outputs) changed as a result of
    this fix.

…signment

Unused-variable tracking only registers a name in `register_variable` at
the moment a single-name assignment binds it. Any read of that name
encountered earlier in traversal order — e.g. a loop condition or body
that reads a name only ever assigned inside the loop, or a name only
ever assigned via tuple/list unpacking, `for`, `with`, or walrus targets
(none of which call `register_variable`) — was silently dropped by
`mark_variable_used`, since it only marked names already present in
`variables`. This produced false-positive "unused variable" diagnostics,
as in facebook#3911, where a `while` loop reads and reassigns two names unpacked
from a tuple.

Give `mark_variable_used` its own scope resolution, mirroring
`look_up_name_for_read`'s walk (skip enclosing class scopes, skip
walrus targets not yet visible in a comprehension's flow, skip
`global`/`nonlocal` captures) but keyed off static scope info instead of
flow, since static scope already knows every name a scope will
eventually bind. When a read resolves to a name that is statically
local to a module/function/method scope but not yet registered, record
it in a new `used_variables_before_registration` set. `register_variable`
now checks that set (alongside the existing `used` flag) when seeding a
newly registered variable's initial `used` state.

This closes the gap for reads that occur before their name's only
registering assignment, without changing behavior for the common case
where a read follows registration.
@meta-cla

meta-cla Bot commented Jul 7, 2026

Copy link
Copy Markdown

Hi @AhmedHedi0!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@github-actions github-actions Bot added the size/l label Jul 7, 2026
@meta-cla

meta-cla Bot commented Jul 7, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@meta-cla meta-cla Bot added the cla signed label Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

False-positive trigger of unused-variable when returning a Generator

1 participant