diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index b5c1c7f5d..d490adf65 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -352,9 +352,17 @@ const bindingOrigin = ( ? binding.directOrigins[0] : undefined; const [initializer] = binding.initializers; - if (binding.mutable || binding.initializers.length !== 1) return undefined; - const origin = semanticRequireOrigin(initializer?.node, state); - const projection = initializer?.projection ?? []; + // A var/let declaration permits writes without proving one occurred. + // Require one unconditional initializer and no actual binding writes. + if ( + initializer === undefined || + binding.initializers.length !== 1 || + binding.definitions.some(({ kind }) => kind === "assignment") || + state.conditionalInitializers.has(initializer.node) + ) + return undefined; + const origin = semanticRequireOrigin(initializer.node, state); + const projection = initializer.projection; return origin === undefined || !projection.every((key): key is string => typeof key === "string") ? undefined diff --git a/tests/composition/javascript-applications/electronApplicationAnalysis.test.ts b/tests/composition/javascript-applications/electronApplicationAnalysis.test.ts index 4180e1818..dd7bd8809 100644 --- a/tests/composition/javascript-applications/electronApplicationAnalysis.test.ts +++ b/tests/composition/javascript-applications/electronApplicationAnalysis.test.ts @@ -116,6 +116,18 @@ describe("static Electron application analysis", () => { expect(handledChannels.sort()).toEqual(["rea:read", "rea:write"]); }); + it("maps unchanged var and let require aliases while excluding reassigned bindings", async () => { + const root = await requireAliasFixtureDirectory(); + + const result = await reconstructJavaScriptArtifact({ input_path: root }); + + expect(result.electron_summary.ipc).toMatchObject({ + main_handlers: 2, + paired_renderer_transmissions: 2, + unpaired_literal_renderer_transmissions: 0, + }); + }); + it("returns a tagged cancellation without executing application code", async () => { const root = await fixtureDirectory(); const controller = new AbortController(); @@ -326,6 +338,50 @@ const fixtureDirectory = async (): Promise => { return root; }; +const requireAliasFixtureDirectory = async (): Promise => { + const root = await createTestTempDirectory("rea-electron-require-aliases-"); + await Promise.all([ + writeFile( + join(root, "package.json"), + JSON.stringify({ + name: "rea-electron-require-aliases", + main: "main.cjs", + }), + ), + writeFile( + join(root, "main.cjs"), + String.raw` +var { ipcMain: main } = require("electron"); +let listener = require("electron/main").ipcMain; +main.handle("rea:read", async () => "value"); +listener.on("rea:write", () => undefined); + +let replaced = require("electron").ipcMain; +replaced = { handle() {} }; +replaced.handle("rea:reassigned", () => undefined); + +var assignedLater; +assignedLater.handle("rea:before-assignment", () => undefined); +assignedLater = require("electron").ipcMain; + +if (globalThis.registerIpc) { + var conditional = require("electron").ipcMain; +} +conditional.handle("rea:conditional", () => undefined); +`, + ), + writeFile( + join(root, "preload.cjs"), + String.raw` +const { ipcRenderer } = require("electron"); +ipcRenderer.invoke("rea:read"); +ipcRenderer.send("rea:write", "value"); +`, + ), + ]); + return root; +}; + const renamedBindingFixtureDirectory = async (): Promise => { const root = await createTestTempDirectory("rea-electron-renamed-"); await Promise.all([