Skip to content

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

@artaommahe

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 1

src/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

  1. artaommahe commented on Jul 30, 2026

    @artaommahe
    Author

    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,049 Method→Method edges, 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 my LIMIT; on the full population it's 4, not 0. The accurate and narrower claim:

    Cross-file resolution works for imported function symbols. It collapses specifically for member calls on instances of imported classes — lsp_ts_method is 99.87% same-file.

    Full-population re-derivation (all 15,837 CALLS edges, no sampling): Method→Method same-file 3,080 of which lsp_ts_method 3,045; cross-file 1,705 — qualified_suffix 663, unique_name 557, import_map 257, suffix_match 215.

    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() in toast.service.ts is character-for-character the same shape as v2, just located in the callee's own file:

    export function sameFileControl(): void {
      const t = new ToastService();
      t.openSuccessUniqueXyz('same-file control');
    }
    caller location relative to callee edge to the method?
    sameFileControl same file ✅ lsp_ts_method, confidence 0.95
    v2 — identical code another file ❌ none
    v1, v3, v4, V5.run, AliasConsumer.run another file ❌ none
    $ trace_path --function-name openSuccessUniqueXyz --direction inbound --depth 1
    callers_total: 1
    cbm-rc-repros.src.lib.toast.toast.service:
      sameFileControl 1
    

    Identical code, one file boundary, and the edge disappears.

    Possibly the same root cause as #1277, and possibly the cause of #514

    Two leads for whoever picks this up

    1. The cross-file path is flaky, not absent. All four working cross-file lsp_ts_method edges come from a single call site shape: a private field declared as a union of two imported classes (private _h: AHandler | BHandler, assigned new AHandler() or new BHandler() in the constructor), then called as this._h.method(...). That is exactly the shape this issue says never resolves. And only one union branch got edges — the four methods on BHandler each have exactly 1 inbound edge, while every method on the sibling AHandler has 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 from pass_calls.c and pass_parallel.c) is asserted in tests/test_registry.c to return true for ("unique_name", is_method=true) — yet the cross-file Method→Method population contains 557 unique_name + 215 suffix_match edges. Either those call sites aren't being flagged is_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, the test column/marker that trace_path --help promises ("test nodes are included with a test column/marker") is absent from the output entirely, even when the sole caller is a .spec.ts file.


    Posted by Claude Code on behalf of @artaommahe

  2. braggintime commented on Jul 30, 2026

    @braggintime

    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:

    const resolver = new EntitlementResolverService(supabase);
    const features = await resolver.resolveFeatures(userId, leagueId);

    resolveFeatures has two real route callers by grep. In the graph it has in_degree: 0, and trace_path(inbound) returns callers: []. As in your Case A, the only edge recorded is the new EntitlementResolverService() constructor call. This holds for both module-scope and function-local instantiation of the service.

    Aggregate for the same repo: 71 CALLS edges from backend/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 are new 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:

    edge type 0.8.1 0.9.0 delta
    CALLS 16,892 10,509 −6,383 (−38%)
    IMPORTS 2,265 3,460 +1,195
    DEFINES 22,028 22,386 +358
    USAGE 15,365 15,687 +322
    (all others) ±9 or less
    total 64,040 59,596 −4,444

    CALLS is the entire delta; the smaller gains elsewhere partially mask it in the headline edge count. On 0.8.1 the resolveFeatures route edges do exist (3 callers, via suffix_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.)

  3. added
    bugSomething isn't working
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    and removed
    windowsWindows-specific issues
    on Aug 3, 2026
  4. added this to the 0.9.1-rc milestone on Aug 3, 2026
  5. DeusData commented on Aug 3, 2026

    @DeusData
    Owner

    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-rc parsing/call-graph bug; the red case should require the cross-file method edge while preserving the same-file behavior.

  6. chonorov commented on Sep 8, 2026

    @chonorov

    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:

    type Service = ReturnType<typeof createService>;
    
    export function useAction(service: Service) {
      return (payload: Payload) => service.updateData(payload);
    }

    The target method is defined on the object returned by the factory in another file. trace_path with inbound direction returns callers_total: 0 and 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_coverage for the cited paths returned metadata_match / no_recorded_issue despite the missing CALLS edge. This can make consumers interpret an empty trace as proof of absence.

    Expected: the cross-file member call produces a CALLS edge, 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.

  7. DeusData commented on Oct 1, 2026

    @DeusData
    Owner

    Thank you again for reporting this, @artaommahe! The fix is now on main in 77ea3bb (merged via #2407), and it will ship in the next release.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingeditor/integrationEditor compatibility and CLI integrationparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions