Repository navigation
B007: don't flag loop variables read by functions defined in the same scope - #600
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate issues affect scope correctness and analysis reliability.
1 open finding
What changed in this PR
Updates B007 to recognize loop-variable reads from nested functions and lambdas using symtable.
Changes:
- Adds closure-aware and module-scope analysis.
- Adds regression tests for scope, shadowing, and pattern matching.
- Updates the changelog.
Unresolved issues remain around global and intermediate-scope bindings, nonlocal stores, repeated symbol-table scans, and source normalization.
| File | Description |
|---|---|
tests/eval_files/b007.py |
Tests closure and shadowing scenarios. |
tests/eval_files/b007_py310.py |
Tests pattern-matching cases. |
tests/eval_files/b007_module_scope.py |
Tests module-level resolution. |
README.rst |
Documents the B007 change. |
bugbear.py |
Implements symbol-table-based closure detection. |
🧠 Review effort: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… scope A loop variable read as a free variable by a function or lambda defined in the same scope as the loop is used. Use the standard library symtable to resolve this like CPython does: free variables of the functions nested in the loop scope, or globals they read for a module-level loop. Fall back to no exemption when the source cannot be compiled. Fixes PyCQA#175.
c6338fa to
82d4177
Compare
cooperlees
left a comment
There was a problem hiding this comment.
Fixed the Copilot finding: when the enclosing scope declares the loop variable global, nested functions that read it have no free variable, so _closure_reads now also counts referenced globals when the name is global in the enclosing symtable table.
Details:
- Direct reads (
global i+def g(): return i) and deeply-nested reads are exempt. - Shadowing still flags B007: names bound locally in an intermediate function (free, not global in the reading table) or by a comprehension, and store-only
globalwrites (is_referenced() == False). nonlocalin the enclosing scope was already handled via free variables (verified with symtable).- Added 4 regression cases to
tests/eval_files/b007.py. - Rebased onto current
main(fixed README.rst changelog conflict; full suite: 128 passed, 2 skipped).
A 'global' declaration makes the loop target global too, in which case nested readers have no free variable. Include referenced globals when the enclosing symtable marks the name global.
82d4177 to
633c973
Compare
cooperlees
left a comment
There was a problem hiding this comment.
Globals fix verified: enclosing-scope global reads (direct and deeply nested) are now exempt, while intermediate-local shadowing, comprehension shadowing, and store-only writes still flag. Rebased onto main, CI + pre-commit green.

Fixes #175
B007 reported a loop control variable as unused when its only readers were a function or lambda defined in the same scope as the loop:
The variable now counts as used when a function defined in that scope reads it. Scopes are resolved with the standard library
symtable, so shadowing (a parameter or local of the same name),nonlocal,globaland class scopes follow CPython: free variables of the nested functions, or, for a loop at module level, the globals they read.symtableis only run when a loop has an unused control variable, once per file; if the source can't be compiled, no exemption is applied.Tests are in
tests/eval_files/b007.py,b007_module_scope.pyandb007_py310.py(the issue example, functions defined before/after the loop, shadowing,global/nonlocal, classes, decorators,match). Running B007 over the CPython 3.13 stdlib and a site-packages tree (5,427 files), the only change is 7 fewer reports, each a loop variable read by a closure (e.g.Lib/test/test_range.py,Lib/test/pickletester.py); none are added. README changelog updated.