fix(javascript): retain proven browser global receivers

This commit is contained in:
morluto committed 2026-10-24 09:11:30 +08:00
1 parent 673a659074
commit 45d0c3e299
5 files changed
+20 -4

No files matched your search

@@ -736,12 +736,21 @@ export function* indexJavaScriptAmbientWritesSteps(
retainSemanticAmbientEffect(state, "write", resolved, node);
}
}
for (const { node, target, browserReceiver, projection } of escapes)
for (const path of ambientWrites(target, node, true, projection).paths)
for (const { node, target, browserReceiver, projection } of escapes) {
const resolved = ambientWrites(target, node, true, projection);
if (
browserReceiver &&
resolved.ownership === "ambient" &&
resolved.paths.length > 0 &&
resolved.paths.every((path) => path.length === 1 && path[0] === "global")
)
state.retainedGlobalCallReceivers.add(target);
for (const path of resolved.paths)
// Only proven browser APIs keep their global receiver from user code.
if (!(browserReceiver && path.length === 1 && path[0] === "global")) {
retainSemanticAmbientEffect(state, "escape", path, node);
}
}
// A function may call a browser method replaced later in source order.
if (
[...browserReceiverMethods].some((method) =>
@@ -1055,6 +1055,7 @@ const createState = (
ambientBindingWrites: new Map(),
ambientWrites: new SemanticPropertyPathCoverage(),
escapingReferenceStores: new WeakSet(),
retainedGlobalCallReceivers: new WeakSet(),
ambientEscapes: new SemanticPropertyPathCoverage(),
ambientEscapeEffects: [],
ambientWriteEffects: [],
@@ -1445,8 +1445,9 @@ export function* collectSemanticMemberMutationsSteps(
)
return;
if (
t.isMemberExpression(expression) ||
t.isOptionalMemberExpression(expression)
(t.isMemberExpression(expression) ||
t.isOptionalMemberExpression(expression)) &&
!state.retainedGlobalCallReceivers.has(expression.object)
)
markEscaped(expression.object, mutation);
};
@@ -171,6 +171,8 @@ export interface JavaScriptSemanticAnalysisState {
readonly ambientBindingWrites: Map<string, t.Node[]>;
/** Source-local writes, tagged by global, import.meta or CJS module identity. */
readonly ambientWrites: SemanticPropertyPathCoverage;
/** Browser receivers proven to be only the ambient global identity. */
readonly retainedGlobalCallReceivers: WeakSet<t.Node>;
/** Store targets whose ownership is unknown or ambient, as classified by ambient indexing. */
readonly escapingReferenceStores: WeakSet<t.MemberExpression>;
/** Ambient object identities passed to calls whose effects are not evaluated. */
@@ -34,6 +34,7 @@ it.each([
'function inspect() { const records = new Map(); return records.get("/accounts"); }',
'const inspect = () => { const records = new Map(); const alias = records; return alias.delete("/accounts"); }',
'const browser = window; browser.open("about:blank"); new Map().get("/accounts")',
'const records = new Map(); const browser = window; browser.open("about:blank"); records.get("/accounts")',
'const box = {}; box.value = { name: "Alice" }; inspect(box); new Map().get("/accounts")',
'patch(() => ({ name: "Alice" })); new Map().get("/accounts")',
'const box = {}; box.prototype = Map.prototype; new Map().get("/accounts")',
@@ -54,6 +55,8 @@ it.each([
it.each([
'client.get("/accounts")',
'const records = new Map(); const browser = window; browser.open(window); records.get("/accounts")',
'const records = new Map(); function invoke() { window.open(); } window.open = patch; invoke(); records.get("/accounts")',
'Object.getPrototypeOf = () => Map.prototype; Object.getPrototypeOf({}).get = fetch; new Map().get("/accounts")',
'Reflect.getPrototypeOf = () => Map.prototype; Reflect.getPrototypeOf({}).get = fetch; new Map().get("/accounts")',
'const records = new Map(); records.get = request; records.get("/accounts")',