Repository navigation
Description
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 projectgetmethod 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:
- one project class defining
get,set, andhas; - a local
Mapusing all three methods; and - 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
- TypeScript: a call through
this.<field>resolves to the ENCLOSING method when the two share a name — silent self-edge, 0% recall on that shape #1496 covers a droppedthis.<field>receiver and its resulting self-edge.
The nested control here is from that same extraction family. The new evidence
is the preserved simple localMapreceiver still falling into the separate
confidence-0.7 fallback. - [python] matching built-ins #1230 fixed literal built-in receivers and lexical reachability. A local
variable whose value is a built-inMapis neither a literal receiver nor a
nested-local candidate. - Local-variable receiver-type inference is C++-only — instance method calls through a local don't resolve in other languages #1108 added positive local-variable type inference. This issue is the
complementary safety property: when receiver typing does not prove a project
class, a sole same-name method must not become a guessed edge.
Environment
Reproduced from fresh indexes with the released CodeGraph 1.5.0 binary on
macOS 26.6 arm64.
Activity
kogriv commentedon Aug 28, 2026
Same defect on Python, and it has a downstream consequence that connects this issue to #888 / #889 / #1012.
Python instance
Two files, no imports between them, one method named
getin the project:DEFAULTS.get(...)isdict.get.read_settingnever touchesLRUCache. Same shape as theMap.getcase above, so this looks like one resolver behaviour rather than two language-specific ones.On a real 207-file Python package the affected band is the usual suspects — call-edge targets by name:
log1654,info691,warning222,get164,calculate114. I checked the sixcallsedges into onegetmethod by hand: all six aredict.getat the call site.Why this reaches further than callers/callees/impact
GraphQueryManager.getFileDependenciesdeliberately walks the resolved call/reference edges rather thanimports(its comment explains why, and the reasoning is sound).findCircularDependenciesis built on it. So a false call edge becomes a false dependency, and a false dependency in the opposite direction of a real import becomes a reported cycle.Two files, fifteen lines, a one-way import:
settings.pyimports nothing. On the 207-file package above,findCircularDependencies()returns 136 cycles; an import-graph cycle check on the same tree returns 1 (a genuine one — two modules in the same subpackage importing each other).That is the part I thought was worth adding here rather than in a new issue: #888 / #889 / #1012 ask for circular-dependency detection to be exposed on the CLI and MCP. Shipping it on top of the current edges would surface this defect to every user of that feature, where today it only affects consumers who reach for the API directly. Fixing #1566 first makes that feature land clean.
Not claimed
findDeadCode()shares the same input, but I did not measure its precision.Found while benchmarking CodeGraph against a Python code-graph tool of my own on a fixed, hash-pinned scope; the full write-up, including the axes where CodeGraph came out ahead, is public in that project's tool card. Environment: CodeGraph 1.6.0 (npm), Linux x64, Node v24.12.0,
DO_NOT_TRACK=1.inth3shadows commentedon Sep 4, 2026
Reproduced on 1.6.0 (npm latest). Adding a minimal fixture and one distinguishing detail: this is not the same root cause as #1683, so a fix for that one will not close this.
Observed
m.set(...)Store.set(src/store.ts)instance-methodm.get(...)Store.get(src/store.ts)instance-methodm.has(...)Store.has(src/store.ts)instance-methodAll three fabricated.
Storeis never imported, constructed or referenced inuse.ts; the only receiver is aMapcreated two lines above.Why this is a distinct defect
The receiver here is a plain identifier with a local, explicit type (
const m = new Map<string, string>()), and resolution reportsinstance-method— it went down the receiver-typing path and picked a project class whose method names happen to match. Contrast #1683, where the receiver is a call expression, is dropped at extraction, and resolvesexact-matchby bare name.So there are two independent paths that both land a call on an unrelated project symbol:
Map, and presumablySet,WeakMap,Array,Promise) and is not excluded before the project-class lookupThe fix direction that would close this one specifically: when local type inference yields a built-in/lib type, treat the call as external and stop, rather than falling through to name-matching against project classes — the same stance
isBuiltInOrExternalalready takes forconsole./Math./JSON., but keyed on the inferred receiver type instead of the callee name.Environment
1.6.0 (and 1.5.0), Linux, fresh
codegraph init, verified via the CLI and direct SQL against.codegraph/codegraph.db.inth3shadows commentedon Sep 4, 2026
Following up my earlier comment with the fix direction turned into actual code.
Where it goes wrong
Every
instance-methodresolution funnels throughresolveMethodOnTypeinsrc/resolution/name-matcher.ts(call sites at :893, :921, :962, :994, :1016). That function validates the method exists on the named type, which is exactly why a wrong inference normally yields no edge — but it never asks whether the inferred type is a project type at all.Mapis not, soMap.getis looked up against project classes andStore.getmatches on name.The existing built-in guard cannot catch this.
BUILTIN_TYPES(src/extraction/tree-sitter.ts:5809) is scalars only:No
Map,Set,WeakMap,WeakSet,Array,Promise,Date,RegExp,Error,Proxy. It also lives in the extractor, so the resolver never consults it.Patch
Guard at the shared choke point, so all five
instance-methodcall sites are covered by one change:Three notes for whoever takes it:
Map<string, string>, so a barehas()on the raw string misses.Mapwould now be skipped. If that matters, gate the check on the name not being inknownNames— though I would argue shadowing a built-in container name is rare enough, and silent-miss is the safer failure, that the simple form is the better default.I have not built or tested this patch, unlike the ones I posted on #1681 and #1683 — I traced the call sites and the
BUILTIN_TYPESgap but did not run it. Treat it as a reviewed proposal, not a verified fix.colbymchenry commentedon Sep 8, 2026
Implemented in #1790, commit
d818b74de411bcadc10a7b022961582e377ec9bd.Receiver inference now stops guessing when a known JS/TS built-in has no matching project method. Untyped nested identifier chains are left unresolved by both extractors, and local project-class receivers still resolve normally.
Verified on Linux x86_64 with Node 22.19.0 after rebuilding TypeScript/assets and the native kernel. Fresh CLI re-indexes and direct SQLite checks produced the same results in the default native engine and with
CODEGRAPH_KERNEL=0:72c1ff13useLocalMapuseNestedMapnew LRUCache()controlNative verification: 920 tests passed across nine related suites, including kernel/wasm parity. Wasm verification: 889 tests passed across seven related suites. The nine new negative regression cases failed before the fix.
PR #1691 was evaluated and not used: the separate
this.<field>.method()family remains tracked by #1496/#1691. The fix is currently on the open PR branch, not merged or released; re-index after upgrading once it is released.fix(resolution): leave built-in Map calls unresolved (#1566) (#1790)
fix(resolution): resolve this.<field>.<method>() on the fields declar…
Revert "fix(resolution): leave built-in Map calls unresolved (colbymc…
docs(design): kernel-only extraction, resolution binding model, green…
fix(kernel): 对齐 wasm 侧的引用坐标口径
kogriv commentedon Oct 15, 2026
Re-measured on 1.6.2, same 207-file Python package and the same hash-pinned staging as my comment above, with 1.6.0 run first through the same harness as a control (it reproduced the original figures exactly).
findCircularDependencies()callsedges fromdict.getsites into the project'sMemoryCache.get[["cache.py","settings.py"]][]So the fix holds on Python too, including the subscript-receiver shape (
MAPPING[key].get(...)), which none of the PR titles named.A correction I owe you. My comment above said an import-graph check on that tree returns "1 (a genuine one)". That was the number of eager cycles. Counting every intra-package import, function-local ones included, the tree has 45. Precision of the 136 is unchanged by this (about 10%), but "136 where there is 1" overstated the gap. The one eager cycle is still the only one that breaks at import time.
Nothing asked; just closing the loop. Method, raw results and the pre-registration are public in the tool card of the project mentioned above.
CodeGraph 1.6.0 and 1.6.2 (npm), Linux x64, Node v24.12.0,
DO_NOT_TRACK=1.