fix(extraction): a TS/JS call through a host-global chain emits no ref (#1707) (#1766)

`chrome.storage.local.get(key)` and `document.body.querySelector(s)` end in
a platform API, but the extractor emitted the bare method name for them. That
name then exact-matched whatever project symbol shared it: in a Chrome
extension every `chrome.storage.local.get/set` inside a storage wrapper bound
to the wrapper's own `get`/`set`, giving self-edges that are not in the
source (#1707).

A member chain whose root identifier is a host object the project never
declares now emits nothing — a silent miss instead of a wrong edge, the same
trade the literal-receiver gate makes (#1230). `window` is deliberately not a
host root: `window.MyNs.doThing()` reaches a project symbol. A chain rooted at
a project value keeps the bare name, so `store.getState().act()`, `ref.value
.m()` and `this.<field>.m()` are untouched.

The Rust kernel mirrors the same gate. Verified on Linux: fail→pass on both
kernel and wasm arms for `__tests__/ts-chained-receiver.test.ts` (2 fail / 1
pass on main → 3/3 with the fix).

Lands / rebases https://github.com/colbymchenry/codegraph/pull/1710 onto
current main.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: Aaron Queen <bompus@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 07:47:28 -05:00
committed by GitHub
co-authored by Colby McHenry Aaron Queen
parent 90dcdbc827
commit a7ea5ba730
3 changed files with 176 additions and 0 deletions
+32
View File
@@ -1065,6 +1065,28 @@ impl<'t> Walker<'t> {
// --- extractCall (TS/JS generic tail) -------------------------------------------------
/// Whether a member-call receiver is a chain rooted at a host object a
/// TS/JS project never declares. `window` is absent on purpose:
/// `window.MyNs.doThing()` reaches a project symbol (#1707).
fn is_host_global_chain(&self, receiver: Node<'t>) -> bool {
const HOST_GLOBAL_ROOTS: [&str; 19] = [
"chrome", "browser", "document", "navigator", "performance", "console",
"localStorage", "sessionStorage", "indexedDB", "crypto", "globalThis",
"process", "Math", "JSON", "Object", "Array", "Reflect", "Promise", "Intl",
];
let mut cur = receiver;
if !matches!(cur.kind(), "member_expression" | "subscript_expression") {
return false;
}
while matches!(cur.kind(), "member_expression" | "subscript_expression") {
match cur.child_by_field_name("object") {
Some(next) => cur = next,
None => return false,
}
}
cur.kind() == "identifier" && HOST_GLOBAL_ROOTS.contains(&self.text(cur))
}
pub(super) fn extract_call(&mut self, node: Node<'t>) {
if self.stack.is_empty() {
return;
@@ -1092,6 +1114,16 @@ impl<'t> Walker<'t> {
if is_literal_receiver(r.kind()) {
return;
}
// A chain rooted at a host namespace — `chrome.storage
// .local.get(k)`, `document.body.querySelector(s)` —
// ends in a platform API, so the bare method name emitted
// here could only exact-match an unrelated project symbol
// sharing it (#1707). Emit nothing. A chain rooted at a
// project value keeps the bare name. Mirrors the TS
// extractor's extractCall (extraction/tree-sitter.ts).
if self.is_host_global_chain(r) {
return;
}
}
let recv_ident = receiver.filter(|r| {
matches!(r.kind(), "identifier" | "simple_identifier" | "field_identifier")