Repository navigation
trace_path data_flow mode doesn't surface arg expressions; NestJS DI patterns defeat ~70% of caller resolution #514
Description
Activity
Hey, thx for reporting. Will check ASAP
- addedbugSomething isn't workingSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgesGraph extraction bugs, false positives, missing edges
on Jun 23, 2026 This issue now has a first-class, durable reproduction in the cumulative bug-reproduction suite landing in #667 — a test that pins the exact failing behaviour, so it's tracked and guarded against silent regression. Where #667 also lands the root-cause fix, that's called out in the PR; otherwise the reproduction makes the eventual fix straightforward and verifiable.
The reproduction goes live once #667 merges. (Posted from the release branch's QA pass.)
- added 2 commits that reference this issue
on Jun 28, 2026 - addedpriority/normalStandard review queue; useful PR with ordinary maintainer urgency.Standard review queue; useful PR with ordinary maintainer urgency.
on Jun 30, 2026 Hit this too, on
0.10.6. I think the root cause is broader than the four mechanisms in the description — plain constructor injection with a concrete class type doesn't resolve either, so none of token binding,ModuleRef.get(), differing field names, or interface-typed fields is required to trigger it.Minimal repro (stock NestJS, nothing dynamic):
// cat.service.ts @Injectable() export class CatService { findAll(): string[] { return ["a"]; } } // cat.controller.ts @Controller("cats") export class CatController { constructor(private readonly catService: CatService) {} @Get() findAll() { return this.catService.findAll(); // <-- no CALLS edge is created } }
trace_path(function_name="CatService.findAll", direction="inbound")returnscallers_total: 0.Note the field name here is the exact camelCase of its type (
catService/CatService), the type is a concrete class, and there is no@Injectand noModuleRef— so cause 3 ("varying field names") and cause 4 ("interface/base-typed fields") don't apply. It looks like injected-property call resolution isn't happening at all, rather than degrading on the harder cases.Two extra observations from a ~2.5k-node TypeScript/NestJS index (
mode: "full", 0 skipped, 0 parse_partial):-
Every
CALLSedge into an injected class originates inside that class, and several are self-loops of the formpostCustomerDevice CALLS postCustomerDevice. That smells like the resolver falling back to the enclosing method's name when it can't resolve the receiver, rather than emitting no edge. -
check_index_coveragereportsstatus: "no_recorded_issue"/freshness: "metadata_match"for the files containing those unresolved calls. Since the docs say to use it to decide whether to trust the graph on a file, it would help a lot if unresolved-receiver call sites were recorded as a coverage gap — right now an emptytrace_pathresult is indistinguishable from "genuinely no callers", which is the failure mode that actually bites, because it's silent.
Happy to test a patch against a real NestJS codebase if that's useful.
-
- addedpriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.and removedpriority/normalStandard review queue; useful PR with ordinary maintainer urgency.Standard review queue; useful PR with ordinary maintainer urgency.
on Sep 5, 2026 Thank you, @thaoula, for the thorough write-up, and @Edwin-Barlow for the minimal repro that let us pin this down exactly!
Gap 1 is fixed in v0.11.0:
trace_pathwithmode: "data_flow"now returns each caller's argument expressions.For Gap 2, your repro still gives zero callers on current main. The cause is specific: TypeScript constructor parameter properties (
constructor(private readonly catService: CatService)) aren't registered as class fields, sothis.catService.findAll()can't be resolved. A normally declared field resolves correctly. That fix is next. The harder DI cases (@Inject(token),ModuleRef.get, base-typed dispatch) and reporting unresolved receivers as a coverage gap are separate design questions we'll follow up on. @Edwin-Barlow, we'd gladly take you up on testing the fix against a real NestJS codebase.- added a commit that references this issue
on Sep 29, 2026 - added a commit that references this issue
on Oct 4, 2026
Version
codebase-memory-mcp 0.8.1 (also reproduced after re-index in
fullmode)Platform
macOS (Apple Silicon)
Install channel
GitHub release archive / install.sh / install.ps1
Binary variant
standard
What happened, and what did you expect?
Two related gaps observed on a NestJS 11 + TypeScript monorepo (~47k nodes, 176k edges after
fullindex).Gap 1 —
arg_expressionsadvertised but never populatedThe
trace_pathtool description promises:data_flow (follow CALLS+DATA_FLOWS with arg expressions). In practice:mode: 'calls'→ 22 callers returned,r.arg_expressionsblank on every CALLS edge in Cyphermode: 'data_flow'withparameter_name: 'delayInMs'→ 18 callers (includes hop-2), but the response has no arg expression field at all — the schema doesn't surface oneConcrete repro — trying to find which callers of
RabbitMqTenantPublisher.publish(topic, data, action?, delayInMs?)pass a non-zerodelayInMs:Expected: per-caller arg expression for
delayInMs(or at minimum a flag indicating which callers pass a non-literal-zero value).Actual: caller list only, zero argument values surfaced. Had to fall back to
rgmultiline search + per-file source reads.The README edge-type list also advertises
DATA_FLOWS with arg-to-param mapping + field access chains— butDATA_FLOWSedges do not appear in the response for this call. Either the indexer is not extracting them for TS, or the tool is not returning them.Gap 2 — NestJS DI defeats LSP caller resolution (~70% miss)
Same publisher, grep finds ~70 callers across the repo.
trace_pathreturns 22. The 48 missed callers all use one of these NestJS patterns:@Inject(Tokens.X) private publisher: RabbitMqTenantPublisher. LSP sees the token type, not the runtime-bound concrete class, so no CALLS edge.ModuleRef.get(RabbitMqTenantPublisher)— pipeline tasks resolve dependencies at runtime (const tenantPublisher = ctx.get(RabbitMqTenantPublisher)). No static edge for the LSP to follow.this.publishervsthis.tenantPublishervsthis.rabbitPublisheracross consumers. Cross-file type resolution fails on the varying declaration sites even when the type is identical.private publisher: RabbitMqPublisherBase(abstract base). Edge resolves to the base method, not the concrete subclass override.Combined, these describe most of the missed callers. NestJS DI design is structurally hostile to static call-graph analysis — but the tool markets itself as NestJS-compatible via TS Hybrid LSP, so the gap is worth documenting.
Impact
trace_pathfor "which callers pass argument N" — they must grep + read source. This is the exact use casedata_flowmode is advertised for.trace_pathfor complete caller inventory on NestJS codebases — ~70% miss rate on DI-injected services is too high for impact analysis.Reproduction
NestJS 11 monorepo, TypeScript. Index with
fullmode. Then:Compare caller count to
rg "\.publish\(" --type tsover the same project — grep finds ~3x more callers, and is the only way to identify which ones pass a non-zero delay.Project scale
~47,623 nodes, 176,553 edges after
fullindex (NestJS 11 + TypeScript monorepo, ~1.5M LOC)Confirmations
@Inject,ModuleRef.get, base-typed fields) are universal to any NestJS project.