From b03df22ce42f87d60f9e348888c3d9af0078ae4c Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Sun, 11 Oct 2026 15:41:00 +0000 Subject: [PATCH] fix(javascript): retain fresh function and class target identity Recognize stable local function, undecorated class and RegExp targets before widening intrinsic mutations. Keep decorated classes opaque because their binding value can be replaced, and preserve class-initialization writes. Cover fresh targets and replacement decorators in inert CLI/MCP fixtures. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> --- docs/javascript-artifact-reconstruction.md | 8 +++++--- src/domain/javascript/electronModuleInterop.test.ts | 10 ++++++++++ src/domain/javascript/javascriptModuleInterop.ts | 8 +++++++- .../applications/electronModuleInteropCli.test.ts | 13 ++++++++++++- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 638cf8fbf6..97e42c4156 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -336,9 +336,11 @@ overwrite the global intrinsic identity. Arbitrary cross-call side effects are n evaluated. Other reassignments, dynamic selections, and visibly overwritten namespace members are unresolved. Known Object/Reflect mutators remain recognizable through literal destructuring -of intrinsic objects. Stable identifier chains to object/array literals keep those -targets separate from intrinsic objects when initialized before use; opaque, -reassigned or not-yet-initialized targets remain unresolved. +of intrinsic objects. Stable identifier chains to fresh object, array, function, +undecorated class or RegExp syntax keep those targets separate from intrinsic +objects when initialized before use; opaque, decorated, reassigned or +not-yet-initialized targets remain unresolved. Writes during class initialization +are still retained. Direct and destructuring assignment, loop, update, and delete targets are checked through namespace aliases; unrelated member writes do not discard observed API identity. This static check does diff --git a/src/domain/javascript/electronModuleInterop.test.ts b/src/domain/javascript/electronModuleInterop.test.ts index 653668d009..d4a275d8f0 100644 --- a/src/domain/javascript/electronModuleInterop.test.ts +++ b/src/domain/javascript/electronModuleInterop.test.ts @@ -251,6 +251,14 @@ describe("Electron module interop capture and scope", () => { "const metadata = {};", "const metadata = [];", "const initial = {}; const metadata = initial;", + "const metadata = () => {};", + "const metadata = function () {};", + "const metadata = async () => {};", + "const metadata = function* () {};", + "const metadata = class {};", + "const metadata = /local/;", + "function metadata() {}", + "class metadata {}", ])( "keeps a fresh mutator target separate from intrinsic identity: %s", (setup) => { @@ -267,6 +275,8 @@ describe("Electron module interop capture and scope", () => { "let metadata = {}; metadata = Object;", "Object.defineProperty(metadata,'x',{value:1}); const metadata = {};", "const metadata = {intrinsic:Object}; Object.defineProperty(metadata.intrinsic,'create',{value:customCreate});", + "const metadata = class {static {Object.create = customCreate;}};", + "@(() => Object) class metadata {}", ])("keeps an unproven mutator target unresolved: %s", (setup) => { const analysis = analyzeJavaScriptStaticSource(` ${setup} Object.defineProperty(metadata,"x",{value:1}); diff --git a/src/domain/javascript/javascriptModuleInterop.ts b/src/domain/javascript/javascriptModuleInterop.ts index 73c13c4127..abfc1c987a 100644 --- a/src/domain/javascript/javascriptModuleInterop.ts +++ b/src/domain/javascript/javascriptModuleInterop.ts @@ -142,7 +142,13 @@ export const createSemanticModuleInterop = ( let current = node; for (;;) { current = unwrapJavaScriptExpression(current).node; - if (t.isObjectExpression(current) || t.isArrayExpression(current)) + if ( + t.isObjectExpression(current) || + t.isArrayExpression(current) || + t.isFunction(current) || + (t.isClass(current) && !current.decorators?.length) || + t.isRegExpLiteral(current) + ) return true; if (!t.isIdentifier(current)) return false; const binding = resolveSemanticBindingState(state, current, current.name); diff --git a/tests/acceptance/applications/electronModuleInteropCli.test.ts b/tests/acceptance/applications/electronModuleInteropCli.test.ts index 758b5ef115..cb4bfc26fb 100644 --- a/tests/acceptance/applications/electronModuleInteropCli.test.ts +++ b/tests/acceptance/applications/electronModuleInteropCli.test.ts @@ -28,6 +28,10 @@ cliTest.for(["CLI", "stdio MCP"] as const)( "projected.cjs", 'const {defineProperty:change} = Object; change(Object,"create",{value:customCreate});', ], + [ + "decorated.cjs", + '@(() => Object) class metadata {}; Object.defineProperty(metadata,"create",{value:customCreate});', + ], ] as const) await writeFile( join(root, name), @@ -40,6 +44,12 @@ cliTest.for(["CLI", "stdio MCP"] as const)( ); const source = `const metadata = {}; Object.defineProperty(metadata,"x",{value:1}); + const localFunction = () => {}; + Object.defineProperty(localFunction,"x",{value:1}); + const localClass = class {}; + Object.defineProperty(localClass,"x",{value:1}); + const localRegExp = /local/; + Object.defineProperty(localRegExp,"x",{value:1}); ${ROLLDOWN_MODULE_INTEROP} let electron = require("electron"); electron = __toESM(electron, 1); @@ -58,6 +68,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)( function decoy(electron) {new electron.BrowserWindow({});} require("./poisoned.cjs"); require("./projected.cjs"); + require("./decorated.cjs"); throw new Error("must not execute main"); `; await writeFile(join(root, "main.cjs"), source); @@ -84,7 +95,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)( preload_entrypoints: 1, }); expect(result.statistics.parse_failures).toBe(0); - expect(result.statistics.parsed_javascript_files).toBe(4); + expect(result.statistics.parsed_javascript_files).toBe(5); const windows = result.graph.nodes.filter( ({ kind }) => kind === "browser-window", );