Repository navigation
fix(python): a parameter or local named like an imported module is not that module - #3046
Merged
Merged
Conversation
…t that module A Python call through a name the file imports as a module, like other.update(...) after `from pkg import other`, linked to the module's function whatever the calling function bound `other` to: a parameter, a local, a with/except/loop target, a walrus, a case capture, a lambda or comprehension variable. resolveViaImport read the file's import mappings alone (its only scope check, pythonFunctionModuleAlias from PR #2600, covers a function-local `import a.b as X`), and isPythonProjectModule, which lets a builtin-named call like update/get/write past the builtin filter for a project module, asked the same file-wide question. The mappings also list from-imports at any depth ahead of plain imports, so a function-local `from pkg import third as other` became every function's `other`. pythonReceiverBinding reads how the code around a reference binds the name: the reference's own statement (lambda parameters, comprehension targets), then each enclosing function, innermost first, through its parameters and own statements (assignment, annotation, for, as, walrus, del, nested def/class, case captures, imports; global keeps the module's binding), memoized per function. pythonImportsInScope gives resolveViaImport's Python branch and isPythonProjectModule only the receiver's mappings that apply there. Validation: - __tests__/python-receiver-rebinding.test.ts, 80 tests (default and wasm, LF and CRLF): 48 fail on the base main (ten rebinding shapes, the local from-import, the cross-function leak), 32 pass (look-alikes: keyword argument, string, comment, comprehension over the module, lambda beside the call, global, keyword pattern, same-module local import); all pass with the fix. The 26 Python-related test files pass. - A/B, main vs fix: django's own package (930 files, kernel path) changes 2 edges' confidence (0.85 to 0.8, same targets: a closure over a parameter `apps` and a local `apps = model._meta.apps`); flask drops one cli.group() call and lowers 4 to 0.7 with the same targets (a nested `def cli` and a pytest fixture parameter `app` named like imports); requests, click, mealie and fastapi (package plus tests), and the express and gin controls are byte-identical. - django package index time, median of 3 interleaved: 11.3 s main, 10.7 s fix. Reimplements PR #2632 by BGamboa13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 30, 2026
inth3shadows
added a commit
to inth3shadows/codegraph
that referenced
this pull request
Oct 30, 2026
Brings 9f379af..bc9291a (colbymchenry#3030, colbymchenry#3031, colbymchenry#3043, colbymchenry#3045, colbymchenry#3046, colbymchenry#3049, colbymchenry#3044, colbymchenry#3047, colbymchenry#3051, colbymchenry#3050, colbymchenry#3052, colbymchenry#3053, colbymchenry#3025, colbymchenry#3055). Clean auto-merge.
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.
Summary
In Python, a call through a name the file imports as a module now reads that module only where nothing closer binds the name. A parameter, a local, a
with … as/except … as/ loop target, a walrus,del, a nesteddeforclass, acasecapture, or a lambda parameter or comprehension variable around the call is that value instead, sodef by_param(other): other.update(…)no longer links into the module the file imports asother. A function-localfrom pkg import third as othernow links that function'sother.get()topkg.third, and no longer every other function's.Reimplements #2632 by @BGamboa13. That PR's headline change (absolute imports of project modules) landed separately with #1921; this is its rebinding guard, written fresh on top of the existing scope readers.
Root cause
resolveViaImportread the file's import mappings alone. Its only scope check was #2600'spythonFunctionModuleAlias, which handles a function-localimport a.b as Xand nothing else, so forother.update(…)withfrom pkg import otherat the top of the file:resolvePythonModuleMemberlinked the call topkg/other.py'supdatewhateverotherheld in the calling function;isPythonProjectModule, which lets a builtin-named method (update,get,write) past the builtin filter when the receiver is a project module, asked the same file-wide question, soother = {}; other.update(…)was waved through as a module call.A related leak: the mappings list
from X import Ystatements at any depth (indented ones included), ahead of every plainimport. A function-localfrom pkg import third as otherwas therefore the first mapping forotherin the whole file, and withimport pkg.other as otherat module level every other function'sother.get()linked topkg.third.Fix
pythonReceiverBinding(name-matcher) answers how the code around a reference binds a name: the reference's own statement (a lambda parameter list or a comprehensionfortarget the reference sits inside, which bind only there), then each enclosing function innermost first (a closure reads its enclosing function's locals). Per function it reads the parameters and the statements of its own body (continuation lines joined, strings blanked, nesteddef/classbodies skipped but their names counted): assignment targets (pythonAssignTargets/pythonTargetBinds, so a keyword argument,x.attr = …andd[x] = …bind nothing), annotations,for,astargets, walrus,del,casecaptures (not a value patternother.MODE, a class patternother(…)or a keyword pattern's attributePoint(other=1)), and imports.global nameleaves the module's binding in place. Results are memoized per function and name; a per-file line index finds the enclosing functions.pythonImportsInScope(import-resolver) gives a dotted Python reference the mappings its receiver means there: none of the receiver's when something else binds it; for a function-local import, only the mappings binding the same thing (a localfrom pkg import otherstill matches the module-level one, a localimport pkg.subkeeps the module-levelimport pkg); and where nothing nearer binds it, only module-level mappings when the receiver's mappings disagree (the leak above), falling back to all of them if no module-level one is found.resolveViaImport's Python branch andisPythonProjectModuleboth use it, andisPythonProjectModulenow also honours Python:import a.b as Xinside a function still gives no call edge (rest of #1626) #2600's localimport a.b as X, asresolvePythonModuleMemberalready did.Resolution only: no extraction change, so no kernel change.
Validation
New
__tests__/python-receiver-rebinding.test.ts, 80 tests over the default and wasm paths, LF and CRLF: on the base main 48 fail (the ten rebinding shapes, the function-localfrom … import … as, the cross-function leak) and 32 pass (the look-alikes: keyword argument, text in a string and a comment, a comprehension over the module, a lambda beside the call,global, a keyword pattern, a same-module local import); all 80 pass with the fix.The triage fixtures (
pr2632-shadow,-shadow2,-localrebind,-scoped,pr2632,-flat): every rebound call (parameter,with … as, assignment,for,except … as, lambda parameter,case other:) is unlinked;local_from.pynow reachesthird.py:get;-scoped's six look-alikes,case Point(other=1),case = 1,top(), the same-module local import and the localimport pkg.third as otherkeep their links; the src-layout and flat headline fixtures are unchanged. A 25-site probe (closures, walrus, annotation-only, augmented, nested class, tupleas,async with,*args/**kw, one-line defs, mapping/sequence captures, later assignment) came out right at every site.A/B, base main vs fix (resolution only, so django ran on the kernel path and the rest on wasm; repo commits in parentheses):
django/package (817df1c4, 930 files, kernel path)_check_lazy_references(apps, …)'s nested function calls through the parameterapps, andlazy_related_operationthroughapps = model._meta.apps, not the module-levelfrom django.apps import appstest_appgroup_app_contextdefinesdef cli()(decorated@click.group(cls=AppGroup)), socliis local there; main had typedcli.group()/cli.command()throughfrom flask.cli import cliwritten inside other test functions.cli.group()is now unlinked (groupis also are.Matchmethod name), the 3cli.command()keepAppGroup.commandat 0.7.client(app)'s pytest fixture parameterappkeepsFlask.test_clientat 0.7.mealie/+tests/(581ac022), fastapifastapi/+tests/(03002a00)Index time, django's package on the kernel path, median of 3 interleaved: 11.3 s main, 10.7 s fix (noise).
Python-named suites plus
frameworks-integration,pr19-improvementsandfunction-ref(26 files): 357 passed, 1 skipped, 0 failed.Full suite on this exact commit (base main c12a7b6, kernel built,
CODEGRAPH_KERNEL_EXPECT=1,CODEGRAPH_NO_DAEMONunset, 4 workers): 795 files, 9,461 passed, 0 failed, 85 skipped.Left out
def f(helper): helper()afterfrom pkg import helper) goes through name matching's own Python local-binding check, which by design defers to an imported name; it is a different path and is not changed here.other = other.sub,try: import x as other / except ImportError: other = None) is out of scope: the guard reads functions and the reference's own statement.@click.group(cls=AppGroup) def cli()): flask's test now leaves onecli.group()unlinked, which main had linked through another function's import (see the triage).🤖 Generated with Claude Code