Skip to content

fix: resolve calls edges through an aliased Python import (#2082)#2093

Closed
Yyunozor wants to merge 1 commit into
Graphify-Labs:v8from
Yyunozor:fix/import-alias-calls-edge
Closed

fix: resolve calls edges through an aliased Python import (#2082)#2093
Yyunozor wants to merge 1 commit into
Graphify-Labs:v8from
Yyunozor:fix/import-alias-calls-edge

Conversation

@Yyunozor

Copy link
Copy Markdown
Contributor

Summary

from pkg import mod as alias records the file-level imports_from edge correctly, but every downstream alias.func() call is dropped — no calls edge is produced, so the callee looks like dead code even though the import graph looks complete. import pkg.mod as alias regresses the same way. The unaliased form resolves fine.

Reproduction

pkg/gate.py:
    def validate(rows):
        return bool(rows)

pkg/c_alias.py:
    from pkg import gate as m_gate
    def run_plain_alias():
        return m_gate.validate([1])
$ graphify extract . --code-only

c_alias.py --imports_from--> pkg/gate.py is present; run_plain_alias --calls--> validate is 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_calls in extract.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_names in extractors/resolution.py, and the aliased_import branch of _import_python) but discarded before it reaches the edges the resolver reads.

Fix

Thread the alias through as a local_alias field on the imports/imports_from edge (same transient-hint pattern as target_file, #1814, popped once consumed so it never reaches graph.json):

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 adjacent import mathlib as m and dotted import pkg.gate as g_alias forms, and the relative from . import gate as r_gate form. All fail on the parent commit with the missing-edge symptom, pass after the fix; one also asserts local_alias never reaches the returned edges.

uv run pytest tests/ -q
3554 passed, 3 skipped, 0 failed

Parent commit, same environment: 3549 passed — delta is exactly these 5 tests. ruff check clean. #2080's calls-edge-direction regression tests pass unchanged.

…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.
safishamsi added a commit that referenced this pull request Jul 22, 2026
@Yyunozor

Copy link
Copy Markdown
Contributor Author

Landed on v8 as cc70085, with the follow-up coverage in 8c5f585 — closing as superseded. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant