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", );