fix(extraction): a TS/JS call through a host-global chain emits no ref (#1707) - #1710
fix(extraction): a TS/JS call through a host-global chain emits no ref (#1707)#1710bompus wants to merge 1 commit into
Conversation
|
The Rust half is now verified. I installed a toolchain and built the kernel, so the caveat in the description is resolved. Setup: I verified by ablation rather than by the parity suite, because it isolates this change:
The TypeScript side was byte-identical in both runs and One thing worth flagging for anyone reproducing this: I could not use Happy to squash, split the Rust hunk out, or adjust the host-global list if you would rather it were narrower or configurable. |
Map of my seven open PRs — posting it here because this is the one with an ordering constraintSeven of these are open and unreviewed, and they're mostly independent, so I don't want the pile to read as one big thing to work through. Here's the map, smallest first, and then the only real dependency — which involves this PR, which is why the comment is here rather than scattered across all seven. Independent, small, no ordering constraints:
#1695 and #1697 are the two I'd merge first — they're tiny, they're user-visible in Claude Code, and they each have a second pair of eyes on them already. Larger, still independent:
The one dependency worth knowing about. This PR (#1710) and @maxmilian's #1686 touch the same two files: More important than the ordering: #1686 needs a kernel half to land with it, not after it. It indexes TypeScript interface members on the TS/wasm path only, and the kernel is what All seven are |
|
Correction to the map above: #1709 is now a draft — don't merge it. I ran it against vitejs/vite after posting that comment. It removes 12 false-positive edges, all correct, and adds 59 new wrong ones (52 #1699 also picked up a real-repo finding — the markdown index itself is solid on vite, but several natural doc queries, including a bare The rest of the map stands: #1695 and #1697 are still the two I'd merge first. |
Real-repo run: the commit itself is clean — but this PR is stacked on #1709, which is notRan this against vitejs/vite, with the Rust kernel built from each arm's own sources — this PR touches Because the branch is 47 commits off The host-global commit: 10 false positives removed, 0 addedEdges 31,002 → 30,992. Ten lost, none gained, and every one is a call through a host global that was previously bound to a same-named project function:
Worth noting for parity: at But do not merge this as it standsThis branch carries #1709's change verbatim — It also carries So merging this today would bring in #1709's regression along with this fix. I've put #1709 back to draft. This one should be rebased off Correction to my stack map aboveI wrote that #1710 and #1686 are independent and would only "want a rebase". That was wrong about the dependency that matters: #1710 is stacked on #1709 and #1706, so it is not independently mergeable at all. I should have checked the branch composition before writing the map rather than reading the PR list at face value. |
|
Reviewed the host-global-root change itself and it is the right cut: a chain rooted at One thing keeps me from taking it into the local integration build: the branch is stacked on the rest of the fork. Against |
`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 two self-edges that are not in the source and 153 fabricated call edges (colbymchenry#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 (colbymchenry#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. Measured on a 588-file extension: 153 false edges removed, no real edge lost (line-insensitive edge diff), false self-edges 3 -> 1. The Rust kernel mirrors the same gate. It is unverified: this host has no cargo toolchain and no prebuilt .node, so only the TS path was executed.
8993c83 to
04320a0
Compare
|
Rebased — the branch is now just the fix, on top of
The markdown index, One correction to your scoping guess: there is no Verified before pushing, same runner both ways on a clean clone,
The delta is exactly the new file and its three tests, and the failure count is identical. Those 894 are pre-existing on untouched The kernel caveat from the commit message still stands: this host has no cargo toolchain and no prebuilt |
|
Correction to my previous comment, and to the commit message: this host does have a Rust toolchain. I have now built the kernel and executed the mirror. The caveat was true when the commit was written and I repeated it without re-checking. cargo/rustc 1.98.1 on What the kernel actually does with the gate. Since TS/JS is in
In both arms the delta is exactly the new test file and its three tests, and the failure count is unchanged. So the "unverified mirror" caveat in the commit message no longer holds, and the PR does not need that re-verification from you. One thing that is not about this PR: the suite is not green on Windows on either arm, including on untouched |
Retraction: the "899 failures" table above is wrong. The real number is 23, and all three causes were mine.The test-results table in my earlier comment reported 899 failures on the kernel arm and 894 on wasm, on The actual figure on an untouched What I did wrong — all three, in order1. I ran the entire suite under Bun. To be precise about why, because I want to be fair to Bun here: this is not Bun faking That is a known, vendor-reproduced bug: oven-sh/bun#40001, with fix PR #40005 still open, which is why a current 1.4.2 still reproduces it. Note also that None of which excuses the run. Every test in this suite that prepares a statement was going to fail, and choosing an unsupported runtime is what put me there. 2. I forced 3. I skipped Stack those together and you get 899. Remove them and you get 23. I compounded it by presenting the numbers in a tidy four-row table with a What this does and does not change about this PRIt does not change the finding this PR rests on. The host-global measurement — vite, 31,002 → 30,992 edges, ten false positives removed and none added — was a graph diff, not a test run, and it reproduces. The kernel-vs-wasm parity check on What it changes is that my "899 failures on The 23, and a fixThey fall into two causes, both Windows-only and both invisible on CI because POSIX unlinks an open file happily where Windows refuses:
I have a test-only branch that fixes both and I'll open it as its own PR shortly. I'm still chasing the last of it: the MCP suites can spawn a detached daemon, which is not a tracked child and has no exit event to wait on, so a few teardowns still intermittently lose a race with the OS releasing its files. I'd rather send that when it's actually clean than repeat today's mistake of publishing a number I haven't earned. Separately, and for the same reason: measurements I post from this box should be assumed to be from a Windows host running Node 26.8.1, which is outside the supported range and which the CLI correctly refuses. Where that matters to a result, I'll say so up front from now on rather than after someone asks. |
Fixes the host-namespace half of #1707.
The bug
extractCallkeeps a receiver only when it is a plain identifier. Every deeper chain collapses to the bare method name, which then exact-matches whatever project symbol shares it. In a Chrome extension that means everychrome.storage.local.get(key)inside a storage wrapper binds to the wrapper's ownget— a self-edge that is not in the source, plus a fabricated caller for every other call site.What this does
Emit nothing 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 could only ever match an unrelated symbol. Same trade the literal-receiver gate already makes (#1230): a silent miss instead of a wrong edge.windowis deliberately not a host root —window.MyNamespace.doThing()reaches a project symbol.Why it is this narrow
I first tried the general version: drop the ref for every receiver the extractor cannot name, keeping only a plain identifier and
this.<field>. It kills the bug and costs far too much. On a 588-file extension it removed 64 correct project edges, and it broke 10 tests in this repo's own 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)The real-world losses had the same shape:
window.MyNs.doThing(), Vue'sintelligence.value.evaluatePlayer(p),obs.liveSources.ingest(row). The bare-name fallback is load-bearing for chained receivers, so a blanket drop is not the fix.Measured
Same repo, same baseline index, line-insensitive call-edge diff (keyed on source/target/refName with multiplicities, so a call that only moved lines is not counted as a loss):
callsedgesimport-resolved edgesinstance-methodedgesThe 32 known failures are environmental on this host (Windows) and identical before and after.
Test
__tests__/ts-chained-receiver.test.ts— fails without the change, passes with it:getdoes not call itself throughchrome.storage.local.getdocument.body.querySelector(s)does not bind to a project function namedquerySelectorwindow.MyNs.ping()andthis.<field>.ping()Scope left open
this.map.get(k)androws[0].text()still bare-name match. Those need the receiver's type, which is #1496's job. The host-global gate is the subset fixable without one.Caveat on the Rust side
The kernel's
extract_call(tsjs/extractors.rs) mirrors the gate, which it has to since TS/JS is routed to the kernel by default. That half is unverified: my host has no cargo toolchain and no prebuilt.node, so only the TypeScript path was executed. Please build and run the kernel tests before merging, or tell me and I will drop the Rust hunk into a follow-up.