fix(javascript): drop built-in effects for visibly replaced globals (#4565)

The semantic collectors admitted Node timers, fetch, WebSocket, JSON.parse
and global coercions from the identifier spelling alone, so a source that
replaced those globals (`setTimeout = () => null`) still produced resolved
built-in effects. Require the existing isUnmodifiedGlobalReference guard,
which the static analyzer already uses for fetch and WebSocket, before
admitting these effects.

The guard treats any visible possible write as a replacement without
evaluating order or reachability, so computed global writes and guarded
polyfills also remove the resolved effect. The workflow docs now say so.

Fixes #3367
This commit is contained in:
amyanger
2026-10-22 19:28:53 +01:00
committed by GitHub
parent de00fdc17d
commit 7436ac8e20
5 changed files with 150 additions and 6 deletions
@@ -218,3 +218,61 @@ describe("JavaScript semantic analysis: event dispatch data", () => {
]);
});
});
describe("JavaScript semantic analysis: replaced built-in globals", () => {
it.each([
["setTimeout", "setTimeout = local; setTimeout(work, 1);", "timer"],
[
"globalThis.setTimeout",
"globalThis.setTimeout = local; setTimeout(work, 1);",
"timer",
],
["fetch", "fetch = local; fetch('https://example.test');", "request"],
[
"WebSocket",
"WebSocket = local; new WebSocket('wss://example.test');",
"request",
],
// A replaced JSON.parse keeps only the generic, partial `.parse` candidate.
["JSON.parse", "JSON.parse = local; JSON.parse('null');", "boundary"],
["JSON", "JSON = { parse: local }; JSON.parse('null');", "boundary"],
["String", "String = local; String(value);", "boundary"],
] as const)(
"does not admit built-in effects after a visible %s replacement",
(_name, source, family) => {
const ir = analyzeJavaScriptSemantics(source);
const operations = {
timer: ir.timerOperations,
request: ir.requestOperations,
boundary: ir.boundaryOperations,
};
expect(
operations[family].filter(
({ resolution }) => resolution === "complete",
),
).toEqual([]);
},
);
it("keeps built-in effects for unmodified globals beside unrelated writes", () => {
const ir = analyzeJavaScriptSemantics(`
JSON.stringify = local; other.setTimeout = local;
setTimeout(work, 1);
fetch('https://example.test');
new WebSocket('wss://example.test');
JSON.parse('null');
String(value);
`);
expect(ir.timerOperations).toMatchObject([
{ method: "setTimeout", resolution: "complete" },
]);
expect(ir.requestOperations.map(({ method }) => method)).toEqual([
"fetch",
"WebSocket",
]);
expect(ir.boundaryOperations).toMatchObject([
{ method: "JSON.parse", resolution: "complete" },
{ method: "String", resolution: "complete" },
]);
});
});