From da0480a665f515505106398260b129cfaaddcf0c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:11:55 +0800 Subject: [PATCH] fix(javascript): preserve capture timing and detached receiver authority --- .../javascriptCommonJsExportReceivers.ts | 8 ++++- ...tSemanticAnalysis.export_lifetimes.test.ts | 36 +++++++++++++++++++ .../javascript/javascriptSemanticValues.ts | 15 +++++++- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/domain/javascript/javascriptCommonJsExportReceivers.ts b/src/domain/javascript/javascriptCommonJsExportReceivers.ts index d435907246..4f98ec043b 100644 --- a/src/domain/javascript/javascriptCommonJsExportReceivers.ts +++ b/src/domain/javascript/javascriptCommonJsExportReceivers.ts @@ -62,7 +62,9 @@ export const commonJsExportApplicability = ( effect: CommonJsExportReceiverEffect | undefined, name: string, ): ReceiverStatus => - effect?.fields?.get(name) ?? effect?.receiver ?? "connected"; + effect?.receiver === "detached" + ? "detached" + : (effect?.fields?.get(name) ?? effect?.receiver ?? "connected"); /** A CommonJS export assignment or `Object.defineProperty` export call. */ export type CommonJsExportSite = t.AssignmentExpression | t.CallExpression; @@ -582,6 +584,10 @@ export function* indexCommonJsExportReceiversSteps( : fresh.has(receiver) && fresh.has(publicIdentity) ? "detached" : "uncertain"; + if (status === "detached") { + effects.set(node, { receiver: status }); + continue; + } const fields = new Map(); for (const { exportedName } of commonJsRuntimeExportValues(node, state)) { const removed = [ diff --git a/src/domain/javascript/javascriptSemanticAnalysis.export_lifetimes.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.export_lifetimes.test.ts index 29c7d93a76..1bcf9e689f 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.export_lifetimes.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.export_lifetimes.test.ts @@ -89,3 +89,39 @@ it("does not copy array holes over retained own object properties", () => { value: 1, }); }); + +it("keeps detached receiver ownership stronger than field deletion uncertainty", () => { + expect( + names( + `Object.defineProperty(exports,"old",{value:()=>1}); delete exports.old; module.exports={live:()=>2};`, + ), + ).toEqual(["default"]); + expect( + names( + `exports.old=()=>1; module.exports={live:()=>2}; if(flag)delete exports.old;`, + ), + ).toEqual(["default"]); +}); + +it("distinguishes copied scalar snapshots from earlier writes and shared reference fields", () => { + const returned = (body: string) => + onlyCallable( + analyzeJavaScriptSemantics(`export function observe(){${body}}`), + "observe", + ).returnSites[0]?.value; + expect( + returned( + `const source={a:1};const copy={...source};source.a=9;return copy.a;`, + ), + ).toEqual({ status: "literal", value: 1 }); + expect( + returned( + `const source={a:1};source.a=9;const copy={...source};return copy.a;`, + )?.status, + ).toBe("unknown"); + expect( + returned( + `const source={child:{a:1}};const copy={...source};source.child.a=9;return copy.child.a;`, + )?.status, + ).toBe("unknown"); +}); diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index c76f701a11..5ebde887a2 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -14,6 +14,7 @@ import { import { semanticCapturePosition, semanticEscapeFollowsCapture, + semanticBindingEffectFollowsCapture, semanticBindingPrecedesCapture, semanticBindingAtRead, } from "./javascriptSemanticMutationInitializers.js"; @@ -546,7 +547,19 @@ const evaluateBinding = ( } let projected = invalidateSemanticMutationPaths( value, - binding.memberWrites.map(({ path }) => path), + binding.memberWrites + .filter( + ({ node }) => + context.capturePoint === undefined || + node === undefined || + !semanticBindingEffectFollowsCapture( + binding, + context.capturePoint, + node, + context.state.parentsByNode, + ), + ) + .map(({ path }) => path), ); for (const escape of binding.escapedPaths) { // Escapes only invalidate object references; other lattice values survive.