fix(resolution): resolve Python module-attribute calls to submodule members (#578)#587
Open
maxmilian wants to merge 1 commit into
Open
Conversation
…embers (colbymchenry#578) A call through an imported module — `mod.helper(...)` where `mod` was bound via `from pkg import mod` or `import pkg.mod as mod` — produced no `calls` edge. The resolver mapped the receiver to the package, not the submodule file, so the member lookup missed and callers/callees/impact/ trace returned empty for the target (a still-used function could look like dead code). Mirror the Go cross-package resolver (colbymchenry#388/colbymchenry#469): map the module-attribute receiver to its module path and resolve the member by an exact root-relative module-path match — which also disambiguates same-named members in different modules. Covers both `from pkg import mod` and `import pkg.mod as mod` forms. Tests: the two required forms, a bare-name no-regression guard, and a cross-module same-name disambiguation case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ae76ca2 to
0112e61
Compare
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
Fixes #578. On Python, a call through an imported module —
mod.helper(...)wheremodwas bound viafrom pkg import modorimport pkg.mod as mod— produced nocallsedge, socodegraph_callers/callees/impact/tracereturned empty for the target even though both ends were indexed. The pattern is common in test suites and namespacing, and a missing edge can make a still-used function look like dead code.This is the same root-cause class already fixed for Go (#388/#469), Java/Kotlin (#314), TypeScript (#359), and C# (#381) — Python was never covered.
Root cause
In
resolveViaImport, the generic import-matching loop maps the receiver's import source to the package (pkg), then looks for the member there. For a module-attribute call the member (helper) lives in the submodule file (pkg/mod.py), so the lookup misses and the edge is silently dropped. The bare-name form (from pkg.mod import helper; helper()) was unaffected because it resolves by plain name.Fix
Add a Python branch in
resolveViaImport(mirroring the Go/Java branches) →resolvePythonModuleAttributeReference:localNameis the receiver, and derive its dotted module path:from pkg import mod→pkg.modimport pkg.mod as m→pkg.mod.py//__init__normalized). The exact match doubles as the disambiguation signal — a same-named member in a different module can't be picked by mistake.No
isExportedfilter for Python (it has no export keyword; module-level defs are importable and their nodes carryisExported: false).Tests (
__tests__/resolution.test.ts)from pkg import mod; mod.helper()resolves topkg/mod.py.import pkg.mod as m; m.helper()resolves topkg/mod.py.from pkg.mod import helper; helper()still resolves.helper; the call througha.modlands ona/mod.pyonly — asserts exactly one edge and the correct target (no wrong edge).Full suite green locally (
npm test— 1098 passing).Scope (intentionally deferred)
To keep the change minimal and sound, these are not handled here and left as follow-ups (all degrade to "no edge", never a wrong edge):
pkg/__init__.pydoesfrom .mod import helper, caller doesimport pkg; pkg.helper()) — needs__init__re-export following.import pkg.modthenpkg.mod.helper()(multi-segment member) — bailed by the single-level member guard.from . import mod— needs resolving.against the file's package dir.Two known, narrow limitations (documented in code, not wrong-edge in normal use):
from pkg import mod; mod = X(); mod.f()could over-attribute — the resolver can't distinguish a shadowing local from the module. Same assumption the Go/Java resolvers make; rare in practice.pkg/mod.pyvspkg/mod/__init__.pyboth present: both normalize to the same stem, so a member found in both would resolve to whichever is found first. This is an ambiguous import Python itself would conflict on.Follow-up
Re-export aggregator support is the most valuable next step and could reuse the existing re-export chain-following machinery; happy to do it in a separate PR if you'd like it in scope.