Repository navigation
TypeScript: cross-file method calls produce no CALLS edge — type-aware tier resolves same-file calls only, so trace_path(inbound) returns 0 callers #1354
Description
Version
codebase-memory-mcp 0.9.1-rc.1
Platform
macOS (Apple Silicon)
Install channel
GitHub release archive / install.sh / install.ps1
Binary variant
standard
What happened, and what did you expect?
In TypeScript, a method call on an instance of a class defined in another file produces no CALLS edge — in any call shape I could construct. Only the new X() constructor call is recorded. trace_path(direction: 'inbound') therefore reports callers_total: 0 for methods that demonstrably have callers, which makes "who calls X" unusable for the dominant shape in OO/Angular TypeScript (this.service.method()).
The type-aware lsp_ts_* tier appears to only ever resolve same-file calls. Sampling 3,000 Method -[:CALLS]-> Method edges on a ~11.8k-file Angular monorepo:
| count | top strategies | |
|---|---|---|
| same-file | 1,983 | lsp_ts_method 1,957 (98.7%) |
| cross-file | 1,017 | qualified_suffix 409, unique_name 319, import_map 145, suffix_match 144 |
Zero of the 1,017 cross-file edges came from lsp_ts_method. So correct cross-file call edges are absent, and the cross-file edges that do exist are all name-based guesses (see the companion issue on misattribution).
Expected: t.someMethod() where t: SomeClass and SomeClass is imported from another file should produce a CALLS edge to SomeClass.someMethod, resolved by the type-aware tier.
Notably not an import-resolution problem: this reproduces identically for a tsconfig.json paths alias import and a plain relative import, and with or without typescript installed in node_modules. (Mentioning because #730 / #767 / #1085 are closed and alias-specific.)
Reproduction
Public repro repo: https://github.com/artaommahe/codebase-memory-mcp-rc-repros — see Case A.
git clone https://github.com/artaommahe/codebase-memory-mcp-rc-repros
cd codebase-memory-mcp-rc-repros
codebase-memory-mcp cli index_repository --repo-path "$PWD" --mode full --name cbm-rc-repros
codebase-memory-mcp cli trace_path --project cbm-rc-repros \
--function-name openSuccessUniqueXyz --direction inbound --depth 1src/app/variants.ts and src/app/alias-consumer.ts contain six calls to ToastService.openSuccessUniqueXyz (a uniquely-named method in src/lib/toast/toast.service.ts) across five distinct call shapes, five via relative import and one via the @lib/* alias.
Observed:
function: openSuccessUniqueXyz
direction: inbound
callers_total: 0
callers: 0 (rows: name hop; qn = group prefix + "." + name)
Every CALLS edge in the whole graph:
codebase-memory-mcp cli query_graph --project cbm-rc-repros \
--query "MATCH (a)-[r:CALLS]->(b) RETURN a.file_path AS caller_file, a.name AS caller, b.qualified_name AS callee, r.strategy AS s, r.confidence AS conf, r.line AS line LIMIT 20"rows: 5 (cols: caller_file caller callee s conf line)
src/app/variants.ts constructor cbm-rc-repros.src.lib.toast.toast.service.ToastService unique_name "0.75" "32"
src/app/report.spec.ts src/app/report.spec.ts cbm-rc-repros.src.app.report.service.ReportService.describe unique_name "0.75" "9"
src/app/variants.ts v1 cbm-rc-repros.src.lib.toast.toast.service.ToastService unique_name "0.75" "8"
src/app/variants.ts v2 cbm-rc-repros.src.lib.toast.toast.service.ToastService unique_name "0.75" "13"
src/app/variants.ts v3 cbm-rc-repros.src.lib.toast.toast.service.ToastService unique_name "0.75" "19"
total: 5
Six real calls to openSuccessUniqueXyz, zero edges to it. The only edges recorded are new ToastService() constructor calls, and even those resolve via unique_name rather than the type-aware tier. v4 (explicitly typed parameter), V5.run (typed class field) and AliasConsumer.run produce no edges at all.
Project scale (if relevant)
Aggregate figures above are from a private Angular monorepo: 11,788 files indexed, 63,840 nodes / 120,805 edges, --mode full, 10,175 .ts files. The linked repro is self-contained dummy code.
Confirmations
- I searched existing issues and this is not a duplicate.
- My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.
Posted by Claude Code on behalf of @artaommahe
Activity
artaommahe commentedon Jul 30, 2026
Correcting one claim in the body that is false, and adding a control that makes the case much sharper. The headline finding stands.
Correction: the type-aware tier is not same-file-only
I wrote that "the type-aware
lsp_ts_*tier appears to only ever resolve same-file calls". That's wrong, and it's refuted by data I already had in the issue:lsp_ts_import: 80 edges, 80 of 80 cross-file. Cross-file type-aware resolution works fine for imported free functions.lsp_ts_method: 3,049Method→Methodedges, of which 4 are cross-file (0.13%).So "zero of the 1,017 cross-file edges came from
lsp_ts_method" was a sampling artifact of myLIMIT; on the full population it's 4, not 0. The accurate and narrower claim:Full-population re-derivation (all 15,837
CALLSedges, no sampling):Method→Methodsame-file 3,080 of whichlsp_ts_method3,045; cross-file 1,705 —qualified_suffix663,unique_name557,import_map257,suffix_match215.Added to the repro: a same-file control, which I think is the strongest form of this
I've pushed a control to the repro repo.
sameFileControl()intoast.service.tsis character-for-character the same shape asv2, just located in the callee's own file:sameFileControllsp_ts_method, confidence 0.95v2— identical codev1,v3,v4,V5.run,AliasConsumer.runIdentical code, one file boundary, and the edge disappears.
Possibly the same root cause as #1277, and possibly the cause of #514
trace_path, incomplete caller sets) is plausibly a symptom of this rather than an independent bug — DI-injected services are precisely "member call on an instance of an imported class".Two leads for whoever picks this up
1. The cross-file path is flaky, not absent. All four working cross-file
lsp_ts_methodedges come from a single call site shape: a private field declared as a union of two imported classes (private _h: AHandler | BHandler, assignednew AHandler()ornew BHandler()in the constructor), then called asthis._h.method(...). That is exactly the shape this issue says never resolves. And only one union branch got edges — the four methods onBHandlereach have exactly 1 inbound edge, while every method on the siblingAHandlerhas 0 inbound edges of any strategy. So there is a working code path here that fires inconsistently, which may be a more tractable entry point than "add cross-file support".2. A guard that should already be preventing the #1355 false positives isn't firing.
cbm_tsjs_suppress_weak_method_match(is_tsjs, is_method, strategy)(src/pipeline/registry.c, called frompass_calls.candpass_parallel.c) is asserted intests/test_registry.cto return true for("unique_name", is_method=true)— yet the cross-fileMethod→Methodpopulation contains 557unique_name+ 215suffix_matchedges. Either those call sites aren't being flaggedis_method, or the guard isn't reached on that path. That single site would explain both the missing edges here and the fabricated ones in #1355.Finally, an unrelated nit found while verifying: with
--include-tests true, thetestcolumn/marker thattrace_path --helppromises ("test nodes are included with a test column/marker") is absent from the output entirely, even when the sole caller is a.spec.tsfile.Posted by Claude Code on behalf of @artaommahe
braggintime commentedon Jul 30, 2026
Independent corroboration from a different codebase, plus a version delta that may help size the impact.
Same symptom, different stack. TypeScript/Express backend (not Angular), 2,250 files, 25,269 nodes / 59,596 edges, 0.9.0 release binary, macOS arm64. The dominant shape here is a service instantiated in a route handler:
resolveFeatureshas two real route callers by grep. In the graph it hasin_degree: 0, andtrace_path(inbound)returnscallers: []. As in your Case A, the only edge recorded is thenew EntitlementResolverService()constructor call. This holds for both module-scope and function-local instantiation of the service.Aggregate for the same repo: 71
CALLSedges frombackend/src/routes→backend/src/services, and when you print them the sources are file nodes and the targets are class names — i.e. essentially all 71 arenew XService()constructor edges, not handler→method edges. A sample of six well-known service methods (useMulligan,submitLineup,modifyLineup,createContest,getLiveLeaderboard,mintCode) has zero inbound edges from the routes directory.Version delta — 0.8.1 vs 0.9.0, identical source tree. A second machine here runs 0.8.1 against the same commit. Edge totals:
CALLSis the entire delta; the smaller gains elsewhere partially mask it in the headline edge count. On 0.8.1 theresolveFeaturesroute edges do exist (3 callers, viasuffix_match/field_type_hint) — they are absent on 0.9.0.So on this repo the cross-file method-call gap looks less like a long-standing limitation and more like a 0.9.0 change: the name-based fallbacks that were covering these calls on 0.8.1 appear to no longer fire, while the type-aware tier still doesn't resolve cross-file — leaving nothing. That's consistent with your finding that zero of 1,017 cross-file edges came from
lsp_ts_method; it suggests the cross-file cases were only ever served by the heuristic tiers, so any tightening there removes them outright.Caveat on provenance: I ran the 0.9.0 figures myself; the 0.8.1 column is from the second machine, same tree, not re-run by me.
Happy to run diagnostics against either index if useful.
(Filed alongside three separate 0.9.0 issues from the same investigation — #1364, #1365, #1366 — which are distinct from this one.)
DeusData commentedon Aug 3, 2026
Thank you for the cross-file versus same-file control and the 0.8.1 to 0.9.0 edge-count comparison. That evidence points to a TypeScript resolution regression rather than absent source extraction. I have removed the unrelated Windows label and routed this as a high-priority
0.9.1-rcparsing/call-graph bug; the red case should require the cross-file method edge while preserving the same-file behavior.chonorov commentedon Sep 8, 2026
Independent confirmation on current stable v0.10.8, macOS arm64, standard GitHub release binary.
A React Native TypeScript project has a cross-file call shaped like this:
The target method is defined on the object returned by the factory in another file.
trace_pathwith inbound direction returnscallers_total: 0and an empty caller set, while a focused source search finds the direct call. The graph was current, the canonical checkout was clean, and there were no stale-index or parse-error symptoms.There is an additional completeness-signal gap:
check_index_coveragefor the cited paths returnedmetadata_match/no_recorded_issuedespite the missingCALLSedge. This can make consumers interpret an empty trace as proof of absence.Expected: the cross-file member call produces a
CALLSedge, or the tooling reports that this relationship is unresolved/incomplete.Actual: the trace is silently empty while coverage looks healthy.
This appears to be the same regression, not a separate issue. No proprietary code, identifiers, or paths are included here.
fix(ts-lsp): type imported class receivers for cross-file method calls (
Merge pull request #2407 from DeusData/fix/issue-1354
DeusData commentedon Oct 1, 2026
Thank you again for reporting this, @artaommahe! The fix is now on
mainin 77ea3bb (merged via #2407), and it will ship in the next release.