You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TS/JS: a call through a chained receiver (chrome.storage.local.get, this.map.get, a.b.text()) reaches the resolver as the bare method name and exact-matches any project symbol with that name #1707
In the TS/JS extractor, a call_expression whose callee is a member_expression keeps the receiver only when the receiver is a plain identifier (recv.method, codegraph-kernel/src/tsjs/extractors.rsextract_call, and the same branch in TreeSitterExtractor.extractCall). Any other receiver, a nested member expression, a subscript, a call, collapses to the bare method name:
source
reference recorded
chrome.storage.local.get([key])
get
this._espnPlayerById.get(id) (a Map)
get
settled.value.text() (a Response)
text
thHeaders[0].text()
text
document.querySelector(sel)
document.querySelector (identifier receiver, kept, and JS_BUILT_INS drops it)
The bare name then goes through matchByExactName, where a single same-named project node wins at confidence 0.9. Nothing downstream can tell that the receiver was a runtime global or a builtin container, because that information is gone before resolution starts.
119 calls edges named get and 117 named set land on an object-literal storage wrapper's get / set methods. Every one of them is a Map#get, Map#set, chrome.storage.local.get/set or URLSearchParams#get call. Two of them are the wrapper's own methods calling chrome.storage.local.get/set, so the wrapper calls itself.
8 calls edges named querySelector land on an interface member AutopickDoc::querySelector (with fix(extraction): index TypeScript interface members (#1638) #1686's interface members; without it they would land on any function so named). The calls are el.shadowRoot.querySelector(...) and this.root.querySelector(...).
4 calls edges named observe / disconnect from MutationObserver instances land on two helper functions in a test file.
#1691 (#1496) fixes the this.<field>.<method> subset by keeping the field and reading its declared type, and declines when the type is not a project class. The non-this shapes above (chrome.…, x.value.text(), arr[0].text()) are outside its scope and still collapse.
Proposed fix
In the extractor, never drop a chained receiver on the floor. For a nested member_expression / subscript_expression / call_expression receiver, record the chain head plus the method, e.g. chrome.storage.local.get → chrome.….get or the full dotted text (the resolver already handles dotted names in matchByQualifiedName / matchMethodCall, and Java, C++ and Go already encode chains). Then:
isBuiltInOrExternal checks the chain head against JS_BUILT_INS, extended with the extension and browser globals that are missing today: chrome, browser, globalThis, navigator, location, history, localStorage, sessionStorage, performance, crypto.
A chained call whose head is a local or a parameter with no inferable type declines, the way matchTsThisFieldCall declines, instead of falling through to bare-name matching. A calls edge that is a guess between Map#get and a project get is worse than no edge: it hides the real callee and inflates every caller list for common method names (get, set, text, push, clear, close).
Happy to open a PR if the direction is right; the head-plus-method encoding is the part I would want a maintainer's view on, since it changes the reference names other resolvers see.
I have a fix on my fork and, more usefully, a measurement of what the obvious version of it costs.
The obvious fix is wrong. My first attempt dropped the ref for every TS/JS receiver the extractor cannot name — a deeper chain, a subscript, a call, an await/paren/as wrapper — keeping only a plain identifier and this.<field>. It does kill the bug, but on this repo (588 files, 41.4k edges) it also removed 64 correct project edges, and it broke 10 tests in the suite:
object-literal-methods — "resolves callers of store actions across files (destructured + chained getState())"
Those all depend on the bare-name fallback for a chained receiver. The real-world losses had the same shape: window.MyNamespace.doThing(), Vue's intelligence.value.evaluatePlayer(p), obs.liveSources.ingest(row). Each reaches a genuine project symbol that only the bare name finds. So the fallback is load-bearing, and a blanket drop trades far more recall than it buys precision.
The narrow version works. Drop the ref only when the receiver is a member/subscript chain whose root identifier is a host object a project never declares: chrome, browser, document, navigator, performance, console, localStorage, sessionStorage, indexedDB, crypto, globalThis, process, Math, JSON, Object, Array, Reflect, Promise, Intl. A chain rooted there ends in a platform API, so the bare name it used to emit could only ever match an unrelated project symbol.
window is deliberately not in that set — window.MyNs.doThing() reaches a project symbol.
Measured on the same repo, against the same baseline:
before
after
fabricated calls edges
153
0
real edges lost (line-insensitive diff)
—
0
false self-edges
3
1
import-resolved edges
4,797
4,797
fork suite
4,052 pass / 32 known
4,052 pass / 32 known / 0 fresh
The 153 were all chrome.storage.local.get/set inside a storage wrapper binding to the wrapper's own get/set — including the two self-edges that are not in the source.
This leaves the rest of the issue unfixed on purpose: this.map.get(k) and rows[0].text() still bare-name match. Those need the receiver's type, which is #1496's job, not the extractor's. The host-global gate is the subset that can be fixed without a type.
Branch: fix/chained-receiver-drop on my fork (bompus/codegraph). It mirrors the gate in the Rust kernel's extract_call (tsjs/extractors.rs) — necessary, since TS/JS is routed to the kernel by default — but the Rust side is unverified: no cargo toolchain and no prebuilt .node on this host, so only the TypeScript path was executed. Happy to open a PR if you want it.
Fixed and landed in #1766 (squash merge onto main).
What was wrong
TS/JS extractCall kept a receiver only for a plain identifier. Deeper chains (chrome.storage.local.get, document.body.querySelector, …) collapsed to the bare method name, which then matchByExactName'd any same-named project symbol (including a storage wrapper calling itself).
What we landed
The host-global subset from @bompus's #1710, rebased onto current main and verified on Linux:
Emit no ref when the receiver is a member/subscript chain whose root is a host object a project never declares (chrome, browser, document, process, Math, …).
Leave window out of that set so window.MyNs.doThing() still resolves.
Mirror in both the TypeScript extractor and the Rust kernel.
Verify (Linux)
arm
main (before)
+ fix
kernel
2 fail / 1 pass
3/3 pass
wasm (CODEGRAPH_KERNEL=0)
2 fail / 1 pass
3/3 pass
Still open (by design)
this.map.get(k) / rows[0].text() still bare-name match — those need receiver typing (#1496 / #1691), not this gate. #1710 is superseded by #1766.
One residue of this still reproduces on main at 8fe3156 (built from source, Linux (WSL2), Node 22.23.2, CODEGRAPH_KERNEL=0), after #1766, the this.<field> typing (#1496) and #2028: a built-in method called on a subscript of a this field collapses to the bare name and exact-matches a project symbol.
// src/stack.tsexportinterfaceUndoStack{push: (entry: string)=>void;}// src/shapes.tsexportclassShapes{privatebyType: Record<string,string[]>={};privatelist: string[]=[];privatemap=newMap<string,string[]>();a(t: string){this.byType[t].push("x");}// -> UndoStack.pushb(){this.list.push("x");}// no edgec(t: string){this.map.get(t)!.push("x");}// no edged(xs: string[][]){xs[0].push("y");}// no edgee(){constlocal: string[][]=[];local[0].push("z");}// no edge}
With a class method push in the project instead of the property (export class Queue { push(x: string) { … } }), the same line links to Queue.push (exact-match, confidence 0.4), and so does this.q[t].push("x") in another class.
So rows[0].push on a parameter or local is now declined, but this.<field>[key].push is not. Found on a real React app, where a test's fake event source (addEventListener doing this.listeners[type].push(handler)) was listed as a caller of an undo-stack hook's push property. I haven't traced which step lets this one shape through; the field's declared type (Record<string, string[]>) would give string[] for the element, so push is Array's.
Activity
fix(extraction): a TS/JS call through a host-global chain emits no ref
bompus commentedon Sep 5, 2026
I have a fix on my fork and, more usefully, a measurement of what the obvious version of it costs.
The obvious fix is wrong. My first attempt dropped the ref for every TS/JS receiver the extractor cannot name — a deeper chain, a subscript, a call, an
await/paren/aswrapper — keeping only a plain identifier andthis.<field>. It does kill the bug, but on this repo (588 files, 41.4k edges) it also removed 64 correct project edges, and it broke 10 tests in the suite:object-literal-methods— "resolves callers of store actions across files (destructured + chainedgetState())"ui-steps-api-servers(3),ui-steps-cross-tier(2),nextjs(2),react-native-bridge(1)Those all depend on the bare-name fallback for a chained receiver. The real-world losses had the same shape:
window.MyNamespace.doThing(), Vue'sintelligence.value.evaluatePlayer(p),obs.liveSources.ingest(row). Each reaches a genuine project symbol that only the bare name finds. So the fallback is load-bearing, and a blanket drop trades far more recall than it buys precision.The narrow version works. Drop the ref only when the receiver is a member/subscript chain whose root identifier is a host object a project never declares:
chrome,browser,document,navigator,performance,console,localStorage,sessionStorage,indexedDB,crypto,globalThis,process,Math,JSON,Object,Array,Reflect,Promise,Intl. A chain rooted there ends in a platform API, so the bare name it used to emit could only ever match an unrelated project symbol.windowis deliberately not in that set —window.MyNs.doThing()reaches a project symbol.Measured on the same repo, against the same baseline:
callsedgesimport-resolved edgesThe 153 were all
chrome.storage.local.get/setinside a storage wrapper binding to the wrapper's ownget/set— including the two self-edges that are not in the source.This leaves the rest of the issue unfixed on purpose:
this.map.get(k)androws[0].text()still bare-name match. Those need the receiver's type, which is #1496's job, not the extractor's. The host-global gate is the subset that can be fixed without a type.Branch:
fix/chained-receiver-dropon my fork (bompus/codegraph). It mirrors the gate in the Rust kernel'sextract_call(tsjs/extractors.rs) — necessary, since TS/JS is routed to the kernel by default — but the Rust side is unverified: no cargo toolchain and no prebuilt.nodeon this host, so only the TypeScript path was executed. Happy to open a PR if you want it.bompus commentedon Sep 5, 2026
Opened as #1710.
fix(extraction): a TS/JS call through a host-global chain emits no ref
fix(extraction): a TS/JS call through a host-global chain emits no ref (
colbymchenry commentedon Sep 8, 2026
Closeout
Fixed and landed in #1766 (squash merge onto
main).What was wrong
TS/JS
extractCallkept a receiver only for a plain identifier. Deeper chains (chrome.storage.local.get,document.body.querySelector, …) collapsed to the bare method name, which thenmatchByExactName'd any same-named project symbol (including a storage wrapper calling itself).What we landed
The host-global subset from @bompus's #1710, rebased onto current
mainand verified on Linux:chrome,browser,document,process,Math, …).windowout of that set sowindow.MyNs.doThing()still resolves.Verify (Linux)
main(before)CODEGRAPH_KERNEL=0)Still open (by design)
this.map.get(k)/rows[0].text()still bare-name match — those need receiver typing (#1496 / #1691), not this gate. #1710 is superseded by #1766.fix(tsjs): extract named object literal members with scoped callers
inth3shadows commentedon Oct 15, 2026
One residue of this still reproduces on
mainat 8fe3156 (built from source, Linux (WSL2), Node 22.23.2,CODEGRAPH_KERNEL=0), after #1766, thethis.<field>typing (#1496) and #2028: a built-in method called on a subscript of athisfield collapses to the bare name and exact-matches a project symbol.With a class method
pushin the project instead of the property (export class Queue { push(x: string) { … } }), the same line links toQueue.push(exact-match, confidence 0.4), and so doesthis.q[t].push("x")in another class.So
rows[0].pushon a parameter or local is now declined, butthis.<field>[key].pushis not. Found on a real React app, where a test's fake event source (addEventListenerdoingthis.listeners[type].push(handler)) was listed as a caller of an undo-stack hook'spushproperty. I haven't traced which step lets this one shape through; the field's declared type (Record<string, string[]>) would givestring[]for the element, sopushis Array's.fix(resolution): calls on the browser's own objects link no project m…