From 45d0c3e299305597670004f8c30cfeb32d997167 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 08:48:03 +0800 Subject: [PATCH] fix(javascript): retain proven browser global receivers --- .../javascript/javascriptSemanticAmbientWrites.ts | 13 +++++++++++-- src/domain/javascript/javascriptSemanticAnalysis.ts | 1 + .../javascript/javascriptSemanticMemberMutations.ts | 5 +++-- src/domain/javascript/javascriptSemanticState.ts | 2 ++ .../javascriptStaticAnalysis.mapEndpoints.test.ts | 3 +++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/domain/javascript/javascriptSemanticAmbientWrites.ts b/src/domain/javascript/javascriptSemanticAmbientWrites.ts index 9853f2bea1..5142c97879 100644 --- a/src/domain/javascript/javascriptSemanticAmbientWrites.ts +++ b/src/domain/javascript/javascriptSemanticAmbientWrites.ts @@ -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) => diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index e629ec3bb9..5c6d86bf87 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -1055,6 +1055,7 @@ const createState = ( ambientBindingWrites: new Map(), ambientWrites: new SemanticPropertyPathCoverage(), escapingReferenceStores: new WeakSet(), + retainedGlobalCallReceivers: new WeakSet(), ambientEscapes: new SemanticPropertyPathCoverage(), ambientEscapeEffects: [], ambientWriteEffects: [], diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index 677a8bd210..341e800a0f 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -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); }; diff --git a/src/domain/javascript/javascriptSemanticState.ts b/src/domain/javascript/javascriptSemanticState.ts index 4c51cb3065..1541b9bc46 100644 --- a/src/domain/javascript/javascriptSemanticState.ts +++ b/src/domain/javascript/javascriptSemanticState.ts @@ -171,6 +171,8 @@ export interface JavaScriptSemanticAnalysisState { readonly ambientBindingWrites: Map; /** 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; /** Store targets whose ownership is unknown or ambient, as classified by ambient indexing. */ readonly escapingReferenceStores: WeakSet; /** Ambient object identities passed to calls whose effects are not evaluated. */ diff --git a/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts b/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts index 4f55849da9..7239802f80 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts @@ -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")',