Skip to content

Resolve TypeScript calls through named imports inside the imported module - #335

Open
L4XB wants to merge 1 commit into
trailhq:mainfrom
L4XB:fix/ts-imported-call-resolution
Open

Resolve TypeScript calls through named imports inside the imported module#335
L4XB wants to merge 1 commit into
trailhq:mainfrom
L4XB:fix/ts-imported-call-resolution

Conversation

@L4XB

@L4XB L4XB commented Sep 9, 2026

Copy link
Copy Markdown

Fixes #330

Problem

For TypeScript/TSX, a bare call to a symbol imported by name from an external package (import { useRouter } from "next/navigation"; … useRouter()) went through resolveName()'s unique-global fallback: if exactly one same-family function with that name existed anywhere in the repo (a test mock, a helper named expect), the call was bound to it with confidence: "inferred". graft callers, hotspots and blast-radius walks then reported production code depending on a mock it never imports, and the edge looked exactly like an extracted one.

The extractor already collected named-import bindings (collectImportedSymbols / collectTsImportBindings, with lexical shadowing handled by withoutShadowedImports) and used them for references, but not for calls.

Change

  • src/graph/extract.ts: a bare TypeScript/TSX call whose callee is a (non-shadowed) named import is emitted with the import's module specifier and its exported name (aliases are unwrapped). Member calls and other languages are untouched.
  • src/graph/resolve.ts: a call edge carrying a specifier is resolved inside that module only:
    • external or unresolved module → the edge is dropped (no repo-wide guess);
    • in-repo module that defines the name → that definition, extracted;
    • in-repo module without the definition (a barrel re-export) → the existing name-based fallback, so barrel imports keep their recall.
  • The ambiguousRepo() fixture in test/graph-traverse-cli.test.ts now imports shared through a barrel: a direct import { shared } from "./a.js" is no longer ambiguous, it correctly resolves to a.ts#shared — which is the point of this change. The A6 zero-hit assertions are unchanged.

Not included: surfacing confidence in EdgeHit / callers --json / MCP output (point 4 of the issue) — that is output plumbing rather than resolution and would be a separate change.

Tests

test/graph-resolve-imported-calls.test.ts:

  • extractor: a call through import { useRouter } from "next/navigation" and an aliased import { helper as h } carry { name, specifier }; a local const useRouter = … shadowing the import keeps the call a bare name;
  • resolver: external specifier → no edge (fails on main, the mock got the edge); no provenance → unchanged unique-name fallback; in-repo module → extracted edge to that module even though a same-named mock exists; in-repo barrel without the name → name-based fallback.

node --import tsx --test test/graph-traverse-cli.test.ts test/graph-resolve-imported-calls.test.ts test/graph-resolve-typed.test.ts: 35 pass; tsc --noEmit clean. (The savings / session-metrics / claude-format suites fail identically on main in my environment — number formatting under a non-English locale — and are unrelated.)

…dule

A bare TypeScript call to a symbol imported by name from an external
package (`import { useRouter } from "next/navigation"`) went through
resolveName()'s unique-name fallback and was bound to any same-named
function in the repository, e.g. a test mock — a false `calls` edge
that reported production code depending on a helper it never imports,
indistinguishable from an extracted edge.

extract.ts now stamps a bare TypeScript call whose callee is a named
import (after lexical shadowing) with the import's module specifier and
exported name. resolve.ts confines such a call to that module: an
external or unresolved module drops the edge; an in-repo module that
defines the name resolves to it alone (`extracted`); an in-repo barrel
that does not define it keeps the name-based fallback. Calls without
import provenance are unchanged.

The ambiguous-name traverse fixture now imports through a barrel, since
a direct import from "./a.js" is no longer ambiguous.

Fixes trailhq#330
@trailhq-graft

trailhq-graft Bot commented Sep 9, 2026

Copy link
Copy Markdown

🌱 graft blast radius

1 area changed → 3 areas can be affected. 4 dependent symbols, depth 2.
Tests: 1 area updated its tests.
Tag: @anirudhkumar-nanonets — 4 of 4 areas · @shhdwi — Graph Resolution, Graph Freshness

flowchart TB
  A0(("Graph Freshness<br/>2 symbols"))
  A1(("Graph Engine<br/>1 symbol"))
  A2(("Pull Request Review<br/>1 symbol"))
  classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
  class A0,A1,A2 reached;
Loading
Can be affected Symbols Nearest hop Reached from
Graph Freshness 2 src/graph/build.ts:L151-L410 buildGraph — calls, depth 1 Graph Resolution
Graph Engine 1 src/engine.ts:L91-L101 graph — calls, depth 2 Graph Resolution
Pull Request Review 1 src/app/review.ts:L45-L99 reviewPullRequest — calls, depth 2 Graph Resolution
Who knows this code — 2 people across 4 areas
Area Who knows it
Graph Resolution · changed @shhdwi — 12 commits, last 27d ago · @anirudhkumar-nanonets — 9 commits, last 15d ago
Graph Freshness · affected @anirudhkumar-nanonets — 18 commits, last 20d ago · @shhdwi — 5 commits, last 28d ago
Graph Engine · affected @anirudhkumar-nanonets — 16 commits, last 1mo ago
Pull Request Review · affected @anirudhkumar-nanonets — 3 commits, last 8d ago

Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no @ has no GitHub handle in its commit email — tag them by hand, or add a .mailmap entry. A suggestion from history, not a CODEOWNERS rule.

All 4 dependent symbols, grouped by area

Graph Freshness — 2 symbols in 2 files

  • src/graph/build.ts:L151-L410 — buildGraph (calls, depth 1)
    170: const rawEdges: RawEdge[] = [];
  • src/graph/refresh.ts:L150-L227 — ensureFreshGraph (calls, depth 2)

Graph Engine — 1 symbol in 1 file

  • src/engine.ts:L91-L101 — graph (calls, depth 2)

Pull Request Review — 1 symbol in 1 file

  • src/app/review.ts:L45-L99 — reviewPullRequest (calls, depth 2)
    79: let body = `${MARKER}\n${markdownReport(report)}`;
Test signal per changed area — 1 ✓

Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.

  • Graph Resolution — 1 of 2 reached · 1 test file changed here: test/graph-resolve-imported-calls.test.ts
    • not reached: walk
35 test suites also reference this code

38 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.

  • test/ask-index.test.ts
  • test/ask.test.ts
  • test/container-extract.test.ts
  • test/context-only-dir.test.ts
  • test/context.test.ts
  • test/covers.test.ts
  • test/generic-extract.test.ts
  • test/graph-cross-language.test.ts
  • test/graph-go.test.ts
  • test/graph-incremental.test.ts
  • test/graph-invariants.test.ts
  • test/graph-java.test.ts
  • test/graph-languages.test.ts
  • test/graph-php.test.ts
  • test/graph-posix-paths.test.ts
  • test/graph-python.test.ts
  • test/graph-r-classes.test.ts
  • test/graph-r-phase3.test.ts
  • test/graph-r-phase4.test.ts
  • test/graph-r-phase5.test.ts
  • …15 more

graft blast · origin/main...HEAD · depth 2 · 4 changed files

Open the interactive graph → — click an area to see its dependent symbols at file:line.

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.

TypeScript external named-import calls resolve to unrelated local same-name functions

1 participant