fix: resolve calls edges through an aliased Python import (#2082)#2093
Open
Yyunozor wants to merge 1 commit into
Open
fix: resolve calls edges through an aliased Python import (#2082)#2093Yyunozor wants to merge 1 commit into
Yyunozor wants to merge 1 commit into
Conversation
…raphify-Labs#2082) `from pkg import mod as alias` correctly emits the file-level `imports_from` edge, but every downstream `alias.func()` call was dropped: the module arm of the cross-file member-call resolver (Graphify-Labs#1883, _resolve_python_member_calls in extract.py) matches a call receiver against the imported module's own file stem, with no awareness that the local binding in the importing file can be a different name. `from pkg import mod` / `mod.func()` resolves because the receiver ("mod") equals the stem; aliasing breaks the match because the receiver ("alias") never does, and the calls edge silently disappears while imports_from stays present -- the graph looks connected, only the symbol-level reverse query comes back empty. `import pkg.mod as alias` regresses the same way through the same resolver. Root cause is a missing propagation, not a missing feature: the local alias is already parsed correctly in two places (_python_imported_names in extractors/resolution.py, and the aliased_import branch of _import_python in extract.py) but discarded before it reaches the edges the module arm reads. Fix threads the alias through as a `local_alias` field on the `imports`/`imports_from` edge (mirroring the existing `target_file` transient-hint pattern, Graphify-Labs#1814, including its pop-once-consumed hygiene so the hint never reaches graph.json): - _import_python now splits the alias off `import pkg.mod as alias` and stamps it on the edge instead of only using it to compute the bare module_name. - _SymbolResolutionFacts.module_imports gains a 4th `local_name` slot so the `from pkg import submod [as alias]` submodule path (Graphify-Labs#1146) carries the binding through to _apply_symbol_resolution_facts, which now stamps `local_alias` on the edge whenever it differs from the submodule's own stem. - The module arm's receiver match now accepts the tracked alias in addition to the module's real stem, keyed per (importing file, target module) so two files aliasing the same module differently each match their own binding. - extract() pops `local_alias` off every edge right after run_language_resolvers runs, and build_from_json drops it from edge attrs too -- the same two spots target_file is dropped at (Graphify-Labs#1814), except the extract()-side pop has to happen AFTER the resolver reads the field, not at the earlier point _disambiguate_colliding_ node_ids already pops target_file: that function runs before run_language_resolvers, so popping local_alias there would strip it before the resolver ever sees it and silently undo the fix above. Known limitation, left out of scope: two different aliases bound to the same module in the same file only resolve the last one registered, since the match is one alias slot keyed per (importing file, target module) -- not a regression, since the parent resolved neither. Adds five regression tests in tests/test_extract.py: the issue's own shape (`from pkg import gate as m_gate`), its try/except-guarded variant (the issue's literal repro, confirming the drop is independent of try: nesting), the adjacent `import mathlib as m` and dotted `import pkg.gate as g_alias` forms, and the relative `from . import gate as r_gate` form -- plus an assertion that `local_alias` never survives into the returned edges. All fail on the parent commit with the exact missing-edge symptom and pass after the fix. Full suite: 3554 passed vs. 3549 on the unfixed parent, a delta of exactly these five new tests; ruff clean. Graphify-Labs#2080's calls-edge-direction regression tests (test_serve.py) still pass unchanged.
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
from pkg import mod as aliasrecords the file-levelimports_fromedge correctly, but every downstreamalias.func()call is dropped — nocallsedge is produced, so the callee looks like dead code even though the import graph looks complete.import pkg.mod as aliasregresses the same way. The unaliased form resolves fine.Reproduction
c_alias.py --imports_from--> pkg/gate.pyis present;run_plain_alias --calls--> validateis missing. Swapping out the alias (from pkg import gate,gate.validate(...)) makes the edge appear.Root cause
The cross-file member-call resolver's module arm (
_resolve_python_member_callsinextract.py, #1883) matches a call receiver against the imported module's own file stem.mod.func()resolves because the receiver equals the stem;alias.func()never does, so the arm bails. The local alias binding is parsed correctly in two places already (_python_imported_namesinextractors/resolution.py, and thealiased_importbranch of_import_python) but discarded before it reaches the edges the resolver reads.Fix
Thread the alias through as a
local_aliasfield on theimports/imports_fromedge (same transient-hint pattern astarget_file, #1814, popped once consumed so it never reaches graph.json):_import_pythonstamps the alias fromimport pkg.mod as alias._SymbolResolutionFacts.module_importsgains alocal_nameslot so thefrom pkg import submod [as alias]submodule path (Package-form imports (from package import module) are not resolved to import edges — creates disconnected test-file islands #1146) carries the binding through.Known limitation
Two different aliases for the same module in one file only resolve the last one registered (one alias slot per importing-file/target-module pair) — not a regression, since the parent resolved neither, and left out of scope here.
Validation
Five regression tests in
tests/test_extract.py: the issue's shape (from pkg import gate as m_gate), its try/except-guarded variant, the adjacentimport mathlib as mand dottedimport pkg.gate as g_aliasforms, and the relativefrom . import gate as r_gateform. All fail on the parent commit with the missing-edge symptom, pass after the fix; one also assertslocal_aliasnever reaches the returned edges.Parent commit, same environment: 3549 passed — delta is exactly these 5 tests.
ruff checkclean. #2080's calls-edge-direction regression tests pass unchanged.