Skip to content

TypeScript: built-in Map.get/set/has calls resolve to unrelated project methods #1566

Description

@netbrah

Summary

TypeScript calls to the built-in Map.get, Map.set, and Map.has methods
resolve to unrelated project methods when the project contains exactly one
same-language method with each name.

Both common receiver shapes fail:

  • a simple local receiver such as values.get(...) is stored as
    values.get, then resolves to the sole project get method with confidence
    0.7 / instance-method; and
  • a nested receiver such as holder.values.get(...) is extracted as bare
    get, then resolves to the project method with confidence 0.9 /
    exact-match.

The simple local receiver is the distinct gap demonstrated here. The nested
receiver belongs to the extraction family already reported in #1496 and is
included as a regression boundary rather than as a second new defect.

The resulting callers, callees, impact, and raw graph all report calls that do
not exist. An unresolved external/built-in call would be safer than a confident
wrong project edge.

Minimal reproduction

One dependency-free TypeScript file:

export class LRUCache {
  private store = new Map<string, string>();

  get(key: string): string | undefined {
    return this.store.get(key);
  }

  set(key: string, value: string): void {
    this.store.set(key, value);
  }

  has(key: string): boolean {
    return this.store.has(key);
  }
}

export function useLocalMap(): boolean {
  const values = new Map<string, string>();
  values.set("answer", "42");
  values.get("answer");
  return values.has("answer");
}

export function useNestedMap(
  holder: { values: Map<string, string> },
): string | undefined {
  return holder.values.get("answer");
}
$ codegraph init

$ codegraph callees useLocalMap --json
{
  "symbol": "useLocalMap",
  "callees": [
    { "name": "set", "kind": "method", "filePath": "repro.ts", "startLine": 8 },
    { "name": "get", "kind": "method", "filePath": "repro.ts", "startLine": 4 },
    { "name": "has", "kind": "method", "filePath": "repro.ts", "startLine": 12 }
  ]
}

$ codegraph callees useNestedMap --json
{
  "symbol": "useNestedMap",
  "callees": [
    { "name": "get", "kind": "method", "filePath": "repro.ts", "startLine": 4 }
  ]
}

The stored edge metadata shows two independent fallbacks:

source          target          confidence  resolvedBy       refName
useLocalMap     LRUCache::set   0.7         instance-method  values.set
useLocalMap     LRUCache::get   0.7         instance-method  values.get
useLocalMap     LRUCache::has   0.7         instance-method  values.has
useNestedMap    LRUCache::get   0.9         exact-match      get

Control

Remove only the LRUCache class, leaving both Map-using functions unchanged,
then initialize a fresh index:

$ codegraph callees useLocalMap --json
{ "symbol": "useLocalMap", "callees": [] }

$ codegraph callees useNestedMap --json
{ "symbol": "useNestedMap", "callees": [] }

The control graph contains zero calls edges. The four call sites that remain
unchanged between the two fixtures acquire false edges only when same-named
project methods become available.

Source-level cause

There are two paths to the same bad outcome.

In src/extraction/tree-sitter.ts, TS/JS method-call extraction preserves a
receiver only when it is a simple identifier. A nested member_expression
such as holder.values or this.store falls through as the bare method name.
That bare name then reaches exact-name resolution, where a sole same-language
candidate wins at confidence 0.9.

For the distinct preserved-simple-receiver case, matchMethodCall in
src/resolution/name-matcher.ts gathers every project method named get,
set, or has. When exactly one same-language method exists, it selects that
method at confidence 0.7 even though receiver inference has not established
that the built-in Map receiver has the project class's type.

A guard that only recognizes an already-inferred receiver type of Map would
not fix the nested case. For simple local receivers, once inference identifies
an external/built-in type rather than a project class, failure to find a project
method should remain unresolved instead of falling back. The recall tradeoff
for a genuinely untyped receiver is separate. For multi-segment TS/JS
receivers, the safe choices are to infer the nested property's type or keep the
call unresolved; preserving more receiver text and then applying the same
unique-method guess would retain the false edge.

Suggested regression

Add a TypeScript fixture beside the local receiver-inference tests with:

  1. one project class defining get, set, and has;
  2. a local Map using all three methods; and
  3. a nested holder.values.get() call.

Assert that none of those built-in calls produces a project calls edge. Keep
the existing positive control where a local variable constructed as a project
class resolves to that class's method.

Relationship to existing issues

Environment

Reproduced from fresh indexes with the released CodeGraph 1.5.0 binary on
macOS 26.6 arm64.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions