Repository navigation
TypeScript: method calls on a receiver typed by inference (const x = new Foo()) produce no CALLS edge #1974
Description
Version
codebase-memory-mcp 0.10.8
Platform
macOS (Apple Silicon)
Install channel
GitHub release archive / install.sh / install.ps1
Binary variant
standard
What happened, and what did you expect?
A method call whose receiver is a const initialised with new Foo() is not resolved: no CALLS edge is created from the calling function to the method. The identical call resolves correctly when the receiver carries an explicit type annotation (const x: Foo = new Foo()).
Expected: both forms produce a CALLS edge. const x = new Foo() is the ordinary way to write this, and the README states that Hybrid LSP performs "parameter binding, return-type inference, generic substitution" so that "the resulting graph mirrors what an IDE 'Go to Definition' would resolve". tsc and tsserver resolve both forms identically.
Impact: on a 5.5k-symbol TypeScript codebase this leaves 752 of 1087 non-test Method nodes (69%) with zero incoming CALLS, versus 36% for Function nodes. That makes trace_path, impact analysis and any "is this dead code" question untrustworthy in one direction — a silent graph is indistinguishable from a genuinely uncalled method.
Reproduction
- Code — two dummy files under /tmp/repro/src:
src/store.ts
export class WidgetStore {
async removeById(id: string): Promise<void> {
void id
}
async findById(id: string): Promise<string> {
return id
}
}
export function helperFn(id: string): string {
return id
}src/service.ts
import { WidgetStore, helperFn } from './store'
const inferred = new WidgetStore()
const annotated: WidgetStore = new WidgetStore()
export async function run(id: string) {
helperFn(id) // A: plain function -> expected CALLS
await inferred.findById(id) // B: inferred receiver -> expected CALLS
await annotated.removeById(id) // C: annotated receiver -> expected CALLS
}- Commands:
codebase-memory-mcp cli index_repository '{"repo_path":"/tmp/repro","mode":"full","name":"repro"}'
codebase-memory-mcp cli query_graph '{"project":"repro","query":"MATCH (a)-[:CALLS]->(b) RETURN a.name AS caller, b.name AS callee"}'
- Result — 3 rows:
src/service.ts WidgetStore
run helperFn
run removeById <- C, annotated receiver: resolved
Expected 4 rows. run -> findById (B, inferred receiver) is absent.
The discriminator is the receiver, not the method: swapping which method each receiver calls swaps which edge disappears. Indexing reports parse_partial: 0, so this is call resolution, not parsing. There is also no incoming edge of any other type into the unresolved method — only DEFINES from its file and class — so USAGE / CALL_REFERENCE do not act as a fallback.
Additional observation, NOT reduced to a minimal case: the same codebase also loses CALLS edges for object-literal repositories — export const fooRepo = { async bar() {…} } called as fooRepo.bar(…) from another module. Minimal reproductions of that shape resolve correctly, so something about the real files (~700 lines, 33 members) is involved and I could not isolate it. Mentioning it only as context for the aggregate: 1482 of 4138 non-test Function nodes have zero incoming CALLS, and an audit of a random sample of 60 found a genuine call-shaped reference elsewhere in the tree for 72% of them.
Logs
Diagnostics trajectory (memory / performance / leak issues)
Project scale (if relevant)
27,757 nodes / 86,358 edges / ~1,900 files
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.
Activity
DeusData commentedon Oct 17, 2026
Thank you @dazaiio for the precise report. Putting the inferred and the annotated receiver side by side made the cause easy to pin down.
This is fixed on main by 3984795 (#1354, merged in PR #2407) and released in v0.12.0:
new ImportedClass()now types the instance through its import binding, so a receiver typed by inference resolves exactly like an annotated one.Verified on current main (9f85f10) with your exact two-file repro (
src/store.ts+src/service.ts):run -> helperFn(lsp_ts_import)run -> findById(lsp_ts_method), through the inferredconst inferred = new WidgetStore()run -> removeById(lsp_ts_method), through the annotated receiver#1999 will add your module-level shape as a regression test. The object-literal repository observation from your report is a separate shape and is not covered by this fix; we are looking at it separately.
Closing as completed. If you still see a missing edge on v0.12.0 or later, please reopen with the snippet.