diff --git a/src/domain/javascript/javascriptSemanticAmbientWrites.ts b/src/domain/javascript/javascriptSemanticAmbientWrites.ts index 5035c6944b..57f53e8509 100644 --- a/src/domain/javascript/javascriptSemanticAmbientWrites.ts +++ b/src/domain/javascript/javascriptSemanticAmbientWrites.ts @@ -650,7 +650,8 @@ export function* indexJavaScriptAmbientWritesSteps( local = true; }, ); - if (!local || ambient.length > 0) + if (!local || ambient.length > 0) { + state.escapingReferenceStores.add(source.target); escapes.push({ node: source.mutation, target: source.node, @@ -659,6 +660,7 @@ export function* indexJavaScriptAmbientWritesSteps( key === null || typeof key === "object" ? null : String(key), ), }); + } } for (const { node, path } of writes) { const target = diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index 8a57125cb5..94647dc741 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -1053,6 +1053,7 @@ const createState = ( requiredModuleEffects: new Map(), ambientBindingWrites: new Map(), ambientWrites: new SemanticPropertyPathCoverage(), + escapingReferenceStores: new WeakSet(), ambientEscapes: new SemanticPropertyPathCoverage(), ambientEscapeEffects: [], ambientWriteEffects: [], diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index 6f49f60b7b..27ab0be6cc 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -1218,7 +1218,27 @@ export function* collectSemanticMemberMutationsSteps( if (writeTarget !== null && !semanticWriteSiteReadsFirst(node)) for (const target of semanticWriteTargets(writeTarget)) writeOnlyMembers.add(target); - for (const source of semanticIterationSources(node, parent)) + for (const source of semanticIterationSources(node, parent)) { + // Local stores retain their sources on member writes. Only the ambient + // identity owner can establish that storing itself exposes a reference. + if ( + source.kind === "reference-store" && + source.target !== undefined && + !state.escapingReferenceStores.has(source.target) && + (localAccessorMethods( + source.target.object, + [ + semanticStaticPropertyKey( + source.target.property, + source.target.computed, + ), + ], + "set", + state, + accessorKeyMayExist, + )?.length ?? 1) === 0 + ) + continue; deferIterable({ ...source, path: source.projection, @@ -1228,6 +1248,7 @@ export function* collectSemanticMemberMutationsSteps( originAt: source.mutation, iterationOnly: source.kind === "iteration", }); + } for (const operand of semanticCoercionOperands(node, (operand) => { const value = evaluateSemanticExpression(operand, state, node); return value.status === "object" || value.status === "array"; diff --git a/src/domain/javascript/javascriptSemanticState.ts b/src/domain/javascript/javascriptSemanticState.ts index ae999625e5..4c51cb3065 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; + /** 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. */ readonly ambientEscapes: SemanticPropertyPathCoverage; /** Capture sites keep later opaque effects from changing earlier copies. */ diff --git a/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts b/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts index 751df70cf2..fad6192813 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts @@ -81,6 +81,7 @@ it.each([ 'const box = {}; box.prototype = Map.prototype; box.prototype.get = fetch; new Map().get("/accounts")', 'const box = {}; box.p = (anything(), Map.prototype); patch(box); new Map().get("/accounts")', 'window.prototype = Map.prototype; new Map().get("/accounts")', + 'const box = { set prototype(value) { patch(value); } }; box.prototype = Map.prototype; new Map().get("/accounts")', 'const box = new ExternalBox(); box.prototype = Map.prototype; new Map().get("/accounts")', 'function expose(box) { box.prototype = Map.prototype; } new Map().get("/accounts")', 'const factory = { make() { return Map.prototype; } }; factory.make().get = fetch; new Map().get("/accounts")',