Resolve TypeScript calls through named imports inside the imported module - #335
Resolve TypeScript calls through named imports inside the imported module#335L4XB wants to merge 1 commit into
Conversation
…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
🌱 graft blast radius1 area changed → 3 areas can be affected. 4 dependent symbols, depth 2. 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;
Who knows this code — 2 people across 4 areas
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 All 4 dependent symbols, grouped by areaGraph Freshness — 2 symbols in 2 files
Graph Engine — 1 symbol in 1 file
Pull Request Review — 1 symbol in 1 file
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.
35 test suites also reference this code38 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.
Open the interactive graph → — click an area to see its dependent symbols at file:line. |
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 throughresolveName()'s unique-global fallback: if exactly one same-family function with that name existed anywhere in the repo (a test mock, a helper namedexpect), the call was bound to it withconfidence: "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 bywithoutShadowedImports) and used them forreferences, but not forcalls.Change
src/graph/extract.ts: a bare TypeScript/TSX call whose callee is a (non-shadowed) named import is emitted with the import's modulespecifierand its exported name (aliases are unwrapped). Member calls and other languages are untouched.src/graph/resolve.ts: a call edge carrying aspecifieris resolved inside that module only:extracted;ambiguousRepo()fixture intest/graph-traverse-cli.test.tsnow importssharedthrough a barrel: a directimport { shared } from "./a.js"is no longer ambiguous, it correctly resolves toa.ts#shared— which is the point of this change. The A6 zero-hit assertions are unchanged.Not included: surfacing
confidenceinEdgeHit/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:import { useRouter } from "next/navigation"and an aliasedimport { helper as h }carry{ name, specifier }; a localconst useRouter = …shadowing the import keeps the call a bare name;main, the mock got the edge); no provenance → unchanged unique-name fallback; in-repo module →extractededge 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 --noEmitclean. (Thesavings/session-metrics/claude-formatsuites fail identically onmainin my environment — number formatting under a non-English locale — and are unrelated.)