You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TypeScript: export const alias = fn breaks caller/impact traversal — callers/impact of the implementation omit every consumer calling through the alias (2-file repro) #1482
When a function is re-exported under a value alias (export const alias = fn), CodeGraph indexes the call edge to the alias and the binding alias = fn, but never composes them. callers and impact on the implementation therefore omit every consumer that calls it through the alias — and report a complete-looking answer with a precise count and no uncertainty marker.
Verified on a 2-file, 6-node repro (below) and reproduced in a 16k-file TypeScript monorepo, where impact on a real function omitted both of its production call sites and listed only the definition file and its tests.
$ codegraph callers realImpl
Callers of "realImpl" (1):
file impl.ts
impl.ts:1
$ codegraph impact realImpl
Impact of changing "realImpl" — 2 affected symbols:
impl.ts
function realImpl:1
file impl.ts:1
consumer.ts does not appear in either. Changing realImpl breaks consumerFn, and impact analysis reports the blast radius as the definition file alone.
The control query shows the missing half of the join is present in the graph:
$ codegraph callers aliasName
Callers of "aliasName" (2):
function consumerFn
consumer.ts:3
file consumer.ts
consumer.ts:1
So the index contains consumerFn → aliasName and aliasName = realImpl. Only the composition is missing.
Expected
callers realImpl and impact realImpl should include consumerFn / consumer.ts, by resolving value aliases whose initializer is a direct reference to an indexed function. Failing that, the output should mark the result as potentially incomplete rather than presenting a count.
Why this is more than a missing edge
The failure is a false negative shaped like a complete answer. There is no "exact match not found" or "possible missing edges" signal, so a consumer cannot distinguish "nothing depends on this" from "the traversal stopped early".
That interacts badly with the MCP steering text. codegraph_explore's description instructs the agent to treat the returned source as already read and not re-open those files (src/mcp/tools.ts:682, src/mcp/server-instructions.ts:32,55), and no environment variable disables it. An agent asking "what breaks if I change this?" is told the blast radius is the definition file, and told not to run the verification that would catch the omission. For impact, which exists to answer exactly that question, silence is indistinguishable from safety.
Related
Same family as the per-language traversal gaps in #1187 (Java field injection), #1373 (C/C++ macros), and #899 (Python module attributes), but for TypeScript value-alias re-export. I could not find an existing report for this pattern.
Follow-up: I reduced this to a pattern matrix, and the alias case in the original report is one symptom of a broader rule. Same 2-file shape for each case — an implementation, an indirection, and a consumer that calls through the indirection — then codegraph callers <impl>:
#
Pattern
Consumer found?
1
export const alias = fn
no
2
export { fn as alias }
no
3
export * from './impl' (barrel)
yes
4
export default fn
no
5
export const api = { run: fn }
no
6
same-file const local = fn; local()
no
7
export const wrapper = () => fn()(control)
yes
8
plain direct import (control)
yes
The three that resolve are exactly the three where the identifier at the call site is spelled the same as the declaration, or where a genuine call to the original name exists. Every case that renames the binding loses the edge.
Cases 2 and 4 are what make this worth raising beyond the original alias report — export { fn as alias } and export default fn are among the most common forms in TypeScript and JavaScript, and both produce a false zero.
Root cause, as far as I can tell
extractImportMappings already models import-side aliasing well — ImportMapping carries localName, exportedName, and isDefault (src/resolution/types.ts:241-251). The gap is on the export/declaration side: I could not find handling for export_clause / export_specifier renames, default-export declaration names, or value bindings whose initializer is a reference to an indexed function. export * from works because extractReExports follows it and no rename is involved.
When export-side resolution can't produce a mapping, resolution falls through to name matching, which cannot cross a rename by construction. Your own comment at src/resolution/import-resolver.ts:775 describes this exact failure for the SFC case:
a barrel import (import { Foo } from './lib') falls back to name-matching — which silently fails whenever the re-export alias differs from the component's real name, yielding a false 0 callers (#629)
The Vue/Svelte/Astro branch fixes it for those languages by feeding the SFC through extractJSImports. Cases 1, 2, 4, 5, and 6 above are the same defect reached through plain .ts files, so the import-side fix doesn't apply.
Why the reporting shape matters more than the missing edges
A missing edge has no confidence value to lower — it is simply absent — so ResolvedRef.confidence can't express it. The result is a false zero rendered as a definitive count, and codegraph_explore's own instruction to treat returned source as already read and not re-open the files removes the one check that would catch it.
Even ahead of any resolver work, marking callers/impact results as potentially incomplete when a symbol is exported under an alias, or when a consuming file's import can't be resolved to a declaration, would turn a silent wrong answer into a caveated one.
Environment
CodeGraph 1.5.0, codegraph-darwin-arm64.tar.gz release bundle, macOS arm64, DO_NOT_TRACK=1. Reproduced through serve --mcp with CODEGRAPH_CATCHUP_GATE_TIMEOUT_MS=0 on a fully reconciled index.
Happy to hand over the matrix repo as a fixture if it's useful — it's 15 files and deterministic.
Correction to my matrix above, and a second defect that turns out to be the bigger one.
I attributed case 4 (export default fn) to export-side alias handling. That was wrong. export default resolves correctly — my fixture imported through a .js specifier, and that is what broke it. Isolating the two:
importp4Defaultfrom"./p4_impl.js";// callers(p4Impl) -> consumer MISSINGimportp4Defaultfrom"./p4_impl";// callers(p4Impl) -> consumer FOUND
Everything else identical. Nothing in the project is named p4Default, so the name-matcher cannot rescue this ref — it isolates import resolution on its own. Confirmed both with and without a "moduleResolution": "NodeNext" tsconfig and "type": "module".
.js specifiers never resolve to .ts sources
EXTENSION_RESOLUTION (src/resolution/import-resolver.ts:22) only ever appends a suffix. For ./x.js the candidates are x.js.ts, x.js.tsx, x.js.d.ts, x.js.js, … and finally bare x.js. The actual file, x.ts, is never tried, so resolveRelativeImport returns null.
Because "moduleResolution": "NodeNext"requires the .js suffix on relative imports, an entire class of modern TypeScript projects resolves no relative import at all. Every reference then falls through to matchReference, which is where the alias failures in my matrix come from. The two defects compound: name matching becomes the only strategy, and name matching cannot cross a rename.
Scale on one real 16k-file TypeScript repo:
relative specifier style
count
./x.js
32,732
extensionless
0
Its full graph — 284k nodes, 1.26M edges — was built with import-based resolution effectively disabled.
Both fixed, with your suite green
I have a working patch for both, against 572d22b:
A rewrite table tried after the literal path, so a real emitted .js on disk still wins and no currently-resolving import changes target: .js → .ts/.tsx/.d.ts, .mjs → .mts, .cjs → .cts. Applied in resolveRelativeImport and resolveAliasedImport.
Export-side alias resolution for the four shapes in the matrix. Node.signature already stores the initializer ("= realImpl"), so the hop needs no re-parsing. Applied once in resolveOne since any strategy can produce an alias target, and only for calls refs — a references edge to the binding is correct as-is. Where a name is ambiguous it produces no hop rather than a guessed edge.
Local export { X as Y } clauses additionally needed getFileExportIndex to index all declarations, not just isExported ones — the extractor does not flag a declaration exported by a separate clause.
Results:
before
after
pattern matrix
3/8
8/8
vitest run
2530 pass / 0 fail
2530 pass / 0 fail
edges on the 16k-file repo
1,262,985
1,268,309 (+5,324)
On the real case that started this: callers went from missing both production call sites to finding both, and impact from 5 symbols to 8 including the correct transitive hop.
Caveat worth stating: I build TS only and run the wasm extraction path, so the kernel-*-parity tests skip. Both patches sit in the TS resolution layer, which the kernel does not implement, but I have not exercised the kernel path.
Happy to open a PR if you'd like it — say the word and whether you want the two fixes split. I would suggest the .js one lands separately regardless, since it is independent of the alias work and much broader in effect.
Combined effect on the 8-case pattern matrix from my earlier comment: 3/8 → 8/8.
Real-world check on an 18.4k-file TS/polyglot repo: re-indexed in 1m30s, edges 1,262,985 → 1,268,309 (+5,324), and the callers query from the original report now returns both true call sites instead of nothing.
Two caveats I want to be explicit about:
The Rust kernel path is unvalidated. I ran with CODEGRAPH_KERNEL=0. Both fixes live in the TS resolution layer above it, but that needs your eyes.
Silent failure remains. There is an unresolved_refs table with status = 'failed' already recording exactly these misses, and callers/impact never surface it. Both PRs leave that alone, but surfacing it looks like the cheapest remaining trust win — a caveated answer beats a confidently wrong one. Happy to open a separate PR if you want it.
Both branches pass the full suite (175 kernel-*-parity skips, expected without a native kernel).
PR:#1808 Commit:e522c3f455846f7dc71d14a1dcf47927a5a7db40 Upstream used: adapted open #1485 (@valkyriweb) onto current main (kept gateTargetKind + defaultBinding; alias hop after resolve).
Root cause
calls through a value alias resolved to the binding, one hop short of the callable, so callers/impact on the implementation silently omitted every consumer that called through the alias.
Activity
valkyriweb commentedon Jul 30, 2026
Follow-up: I reduced this to a pattern matrix, and the alias case in the original report is one symptom of a broader rule. Same 2-file shape for each case — an implementation, an indirection, and a consumer that calls through the indirection — then
codegraph callers <impl>:export const alias = fnexport { fn as alias }export * from './impl'(barrel)export default fnexport const api = { run: fn }const local = fn; local()export const wrapper = () => fn()(control)The three that resolve are exactly the three where the identifier at the call site is spelled the same as the declaration, or where a genuine call to the original name exists. Every case that renames the binding loses the edge.
Cases 2 and 4 are what make this worth raising beyond the original alias report —
export { fn as alias }andexport default fnare among the most common forms in TypeScript and JavaScript, and both produce a false zero.Root cause, as far as I can tell
extractImportMappingsalready models import-side aliasing well —ImportMappingcarrieslocalName,exportedName, andisDefault(src/resolution/types.ts:241-251). The gap is on the export/declaration side: I could not find handling forexport_clause/export_specifierrenames, default-export declaration names, or value bindings whose initializer is a reference to an indexed function.export * fromworks becauseextractReExportsfollows it and no rename is involved.When export-side resolution can't produce a mapping, resolution falls through to name matching, which cannot cross a rename by construction. Your own comment at
src/resolution/import-resolver.ts:775describes this exact failure for the SFC case:The Vue/Svelte/Astro branch fixes it for those languages by feeding the SFC through
extractJSImports. Cases 1, 2, 4, 5, and 6 above are the same defect reached through plain.tsfiles, so the import-side fix doesn't apply.Why the reporting shape matters more than the missing edges
A missing edge has no confidence value to lower — it is simply absent — so
ResolvedRef.confidencecan't express it. The result is a false zero rendered as a definitive count, andcodegraph_explore's own instruction to treat returned source as already read and not re-open the files removes the one check that would catch it.Even ahead of any resolver work, marking
callers/impactresults as potentially incomplete when a symbol is exported under an alias, or when a consuming file's import can't be resolved to a declaration, would turn a silent wrong answer into a caveated one.Environment
CodeGraph 1.5.0,
codegraph-darwin-arm64.tar.gzrelease bundle, macOS arm64,DO_NOT_TRACK=1. Reproduced throughserve --mcpwithCODEGRAPH_CATCHUP_GATE_TIMEOUT_MS=0on a fully reconciled index.Happy to hand over the matrix repo as a fixture if it's useful — it's 15 files and deterministic.
valkyriweb commentedon Jul 30, 2026
Correction to my matrix above, and a second defect that turns out to be the bigger one.
I attributed case 4 (
export default fn) to export-side alias handling. That was wrong.export defaultresolves correctly — my fixture imported through a.jsspecifier, and that is what broke it. Isolating the two:Everything else identical. Nothing in the project is named
p4Default, so the name-matcher cannot rescue this ref — it isolates import resolution on its own. Confirmed both with and without a"moduleResolution": "NodeNext"tsconfig and"type": "module"..jsspecifiers never resolve to.tssourcesEXTENSION_RESOLUTION(src/resolution/import-resolver.ts:22) only ever appends a suffix. For./x.jsthe candidates arex.js.ts,x.js.tsx,x.js.d.ts,x.js.js, … and finally barex.js. The actual file,x.ts, is never tried, soresolveRelativeImportreturns null.Because
"moduleResolution": "NodeNext"requires the.jssuffix on relative imports, an entire class of modern TypeScript projects resolves no relative import at all. Every reference then falls through tomatchReference, which is where the alias failures in my matrix come from. The two defects compound: name matching becomes the only strategy, and name matching cannot cross a rename.Scale on one real 16k-file TypeScript repo:
./x.jsIts full graph — 284k nodes, 1.26M edges — was built with import-based resolution effectively disabled.
Both fixed, with your suite green
I have a working patch for both, against
572d22b:.json disk still wins and no currently-resolving import changes target:.js→.ts/.tsx/.d.ts,.mjs→.mts,.cjs→.cts. Applied inresolveRelativeImportandresolveAliasedImport.Node.signaturealready stores the initializer ("= realImpl"), so the hop needs no re-parsing. Applied once inresolveOnesince any strategy can produce an alias target, and only forcallsrefs — areferencesedge to the binding is correct as-is. Where a name is ambiguous it produces no hop rather than a guessed edge.Local
export { X as Y }clauses additionally neededgetFileExportIndexto index all declarations, not justisExportedones — the extractor does not flag a declaration exported by a separate clause.Results:
vitest runOn the real case that started this:
callerswent from missing both production call sites to finding both, andimpactfrom 5 symbols to 8 including the correct transitive hop.Caveat worth stating: I build TS only and run the wasm extraction path, so the
kernel-*-paritytests skip. Both patches sit in the TS resolution layer, which the kernel does not implement, but I have not exercised the kernel path.Happy to open a PR if you'd like it — say the word and whether you want the two fixes split. I would suggest the
.jsone lands separately regardless, since it is independent of the alias work and much broader in effect.valkyriweb commentedon Jul 30, 2026
Both root causes now have PRs, independent of each other and landable in either order:
.jsimport specifiers to their TS source #1484 —.jsspecifiers resolve to their TS source. This is the bigger one: under"moduleResolution": "NodeNext"no relative import resolved at all, so every reference fell through to name-matching.export const alias = fn,export { fn as alias },export const api = { run: fn }, same-fileconst local = fn) now hop to the function they name.Combined effect on the 8-case pattern matrix from my earlier comment: 3/8 → 8/8.
Real-world check on an 18.4k-file TS/polyglot repo: re-indexed in 1m30s, edges 1,262,985 → 1,268,309 (+5,324), and the
callersquery from the original report now returns both true call sites instead of nothing.Two caveats I want to be explicit about:
CODEGRAPH_KERNEL=0. Both fixes live in the TS resolution layer above it, but that needs your eyes.unresolved_refstable withstatus = 'failed'already recording exactly these misses, andcallers/impactnever surface it. Both PRs leave that alone, but surfacing it looks like the cheapest remaining trust win — a caveated answer beats a confidently wrong one. Happy to open a separate PR if you want it.Both branches pass the full suite (175
kernel-*-parityskips, expected without a native kernel).fix(resolution): resolve `.js` import specifiers to their TS source
fix(resolution): follow alias bindings to the function they name
colbymchenry commentedon Sep 9, 2026
Fixed on Linux — landed via Forge PR
PR: #1808
Commit:
e522c3f455846f7dc71d14a1dcf47927a5a7db40Upstream used: adapted open #1485 (@valkyriweb) onto current
main(keptgateTargetKind+defaultBinding; alias hop after resolve).Root cause
callsthrough a value alias resolved to the binding, one hop short of the callable, socallers/impacton the implementation silently omitted every consumer that called through the alias.Linux verify (fail → pass)
Exact issue repro (
from "./impl.js"):callers realImplconsumerFnmissingconsumerFnpresentimpact realImplconsumer.tsmissingconsumerFn/consumer.tspresentcallers aliasName(control)consumerFnFocused tests: 6/6 (
alias-binding-resolution). Related resolver regressions: 231/231.Closing as completed. #1485 will be closed as superseded by #1808.
fix(resolution): follow alias bindings to the function they name (#1482…
perf(resolution): stop re-reading resolved targets and re-probing mis…
perf(resolution): remove the indexing regressions since v1.6.0 (vscod…