From 283a6e89043f5febf481e58defc3cf1ddc58e8c8 Mon Sep 17 00:00:00 2001 From: ShuaixinHuang <1836656514@qq.com> Date: Fri, 30 Oct 2026 10:59:00 +0800 Subject: [PATCH 1/5] fix(javascript): resolve module-level Electron options in functions --- .../electronStaticAnalysisOptions.test.ts | 38 +++++++++++++++++++ .../javascriptSemanticMutationInitializers.ts | 32 ++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/domain/javascript/electronStaticAnalysisOptions.test.ts b/src/domain/javascript/electronStaticAnalysisOptions.test.ts index e164a237c2b..23813c260c9 100644 --- a/src/domain/javascript/electronStaticAnalysisOptions.test.ts +++ b/src/domain/javascript/electronStaticAnalysisOptions.test.ts @@ -200,6 +200,44 @@ describe("Electron option captures and initialization", () => { }); }); +it("resolves module-level webPreferences used inside a function", () => { + const analysis = analyzeJavaScriptStaticSource(` + const { BrowserWindow } = require("electron"); + const webPreferences = { nodeIntegration: true, contextIsolation: false }; + function createWindow() { + return new BrowserWindow({ width: 800, webPreferences }); + } + createWindow(); + `); + const window = analysis.electron.browser_windows[0]; + expect(window?.web_preferences_status).toBe("object-literal"); + expect(window?.web_preferences).toEqual( + expect.arrayContaining([ + { + name: "nodeIntegration", + value: expect.objectContaining({ status: "literal", value: true }), + }, + { + name: "contextIsolation", + value: expect.objectContaining({ status: "literal", value: false }), + }, + ]), + ); +}); +it("keeps a module-level option unresolved when called before initialization", () => { + const analysis = analyzeJavaScriptStaticSource(` + const { BrowserWindow } = require("electron"); + function createWindow() { + return new BrowserWindow({ webPreferences }); + } + createWindow(); + const webPreferences = { nodeIntegration: true }; + `); + expect(analysis.electron.browser_windows[0]).toMatchObject({ + web_preferences_status: "dynamic", + web_preferences: [], + }); +}); describe("Electron option evidence", () => { it.each([ '({ webPreferences: { preload: "./preload.js", sandbox: true } } as const)', diff --git a/src/domain/javascript/javascriptSemanticMutationInitializers.ts b/src/domain/javascript/javascriptSemanticMutationInitializers.ts index 2276d8a1ba2..5ffc12cee7d 100644 --- a/src/domain/javascript/javascriptSemanticMutationInitializers.ts +++ b/src/domain/javascript/javascriptSemanticMutationInitializers.ts @@ -897,11 +897,43 @@ export const semanticBindingAtRead = ( binding, hasDefiniteInitializer, }; + const functionStatementPosition = (): StatementPosition | null => { + let child: t.Node = read; + for ( + let parent = parents.get(child); + parent !== undefined; + child = parent, parent = parents.get(parent) + ) { + if ( + t.isFunction(parent) && + (child === parent.body || semanticComputedMethodKey(parent, child)) + ) { + if (t.isStatement(parent)) return statementPosition(parent, parents); + for ( + let owner = parents.get(parent); + owner !== undefined; + owner = parents.get(owner) + ) + if (t.isStatement(owner)) return statementPosition(owner, parents); + return null; + } + } + return null; + }; + const functionPosition = functionStatementPosition(); + const moduleConstInitializerIsBeforeFunction = + sourcePosition !== null && + functionPosition !== null && + sourcePosition.body === functionPosition.body && + positionPrecedes(sourcePosition, functionPosition) && + binding.scopeId.startsWith("scope:program:"); const uncertainOrder = repeatingLoop || sourcePosition === null || position === null || sourcePosition.body !== position.body; + if (uncertainOrder && moduleConstInitializerIsBeforeFunction) + return { status: "ordered", binding, hasDefiniteInitializer: true }; if (uncertainOrder) return { status: "unresolved", binding, hasDefiniteInitializer }; const selected = selectInitializers(binding, position, read, parents); From 4edd03e3b337cdf313b7eb86f0c6b0df31140885 Mon Sep 17 00:00:00 2001 From: ShuaixinHuang <1836656514@qq.com> Date: Fri, 30 Oct 2026 11:20:41 +0800 Subject: [PATCH 2/5] fix(javascript): limit module option ancestry walk --- .../javascriptSemanticMutationInitializers.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/domain/javascript/javascriptSemanticMutationInitializers.ts b/src/domain/javascript/javascriptSemanticMutationInitializers.ts index 5ffc12cee7d..396711aaf8e 100644 --- a/src/domain/javascript/javascriptSemanticMutationInitializers.ts +++ b/src/domain/javascript/javascriptSemanticMutationInitializers.ts @@ -920,13 +920,20 @@ export const semanticBindingAtRead = ( } return null; }; - const functionPosition = functionStatementPosition(); + const moduleConstBinding = + binding.scopeId.startsWith("scope:program:") && + binding.declarationKinds.size === 1 && + binding.declarationKinds.has("const") && + !binding.mutable; + const functionPosition = moduleConstBinding + ? functionStatementPosition() + : null; const moduleConstInitializerIsBeforeFunction = + moduleConstBinding && sourcePosition !== null && functionPosition !== null && sourcePosition.body === functionPosition.body && - positionPrecedes(sourcePosition, functionPosition) && - binding.scopeId.startsWith("scope:program:"); + positionPrecedes(sourcePosition, functionPosition); const uncertainOrder = repeatingLoop || sourcePosition === null || From fb2532ad61ceb4cce7d233131fc1edd4305925ff Mon Sep 17 00:00:00 2001 From: ShuaixinHuang <1836656514@qq.com> Date: Fri, 30 Oct 2026 11:51:26 +0800 Subject: [PATCH 3/5] fix(javascript): preserve uncertain captured argument ordering --- .../javascript/javascriptSemanticMutationInitializers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/javascript/javascriptSemanticMutationInitializers.ts b/src/domain/javascript/javascriptSemanticMutationInitializers.ts index 396711aaf8e..c7d594c394f 100644 --- a/src/domain/javascript/javascriptSemanticMutationInitializers.ts +++ b/src/domain/javascript/javascriptSemanticMutationInitializers.ts @@ -940,7 +940,8 @@ export const semanticBindingAtRead = ( position === null || sourcePosition.body !== position.body; if (uncertainOrder && moduleConstInitializerIsBeforeFunction) - return { status: "ordered", binding, hasDefiniteInitializer: true }; + // Initialization coverage does not order a capture in another activation. + return { status: "unresolved", binding, hasDefiniteInitializer: true }; if (uncertainOrder) return { status: "unresolved", binding, hasDefiniteInitializer }; const selected = selectInitializers(binding, position, read, parents); From 9bafc838629358fb8ebc0ce86abefa5c82c8a0ab Mon Sep 17 00:00:00 2001 From: ShuaixinHuang <1836656514@qq.com> Date: Fri, 30 Oct 2026 13:38:37 +0800 Subject: [PATCH 4/5] fix(javascript): retain hoisted activation uncertainty --- .../electronStaticAnalysisOptions.test.ts | 57 ++++++++ .../javascriptSemanticMutationInitializers.ts | 132 ++++++++++++++---- .../electronModuleInitializationCli.test.ts | 61 ++++++++ 3 files changed, 225 insertions(+), 25 deletions(-) create mode 100644 tests/acceptance/applications/electronModuleInitializationCli.test.ts diff --git a/src/domain/javascript/electronStaticAnalysisOptions.test.ts b/src/domain/javascript/electronStaticAnalysisOptions.test.ts index 23813c260c9..69623c0bb99 100644 --- a/src/domain/javascript/electronStaticAnalysisOptions.test.ts +++ b/src/domain/javascript/electronStaticAnalysisOptions.test.ts @@ -238,6 +238,63 @@ it("keeps a module-level option unresolved when called before initialization", ( web_preferences: [], }); }); +it.each([ + { label: "a direct call", before: "createWindow();", after: "" }, + { + label: "an aliased call", + before: "const early = createWindow; early();", + after: "", + }, + { + label: "a forwarding function", + before: "start();", + after: "function start() { createWindow(); }", + }, +])( + "does not initialize a hoisted window function reached by $label", + ({ before, after }) => { + const source = ` + const { BrowserWindow } = require("electron"); + ${before} + const webPreferences = { nodeIntegration: true }; + function createWindow() { + return new BrowserWindow({ webPreferences }); + } + ${after} + `; + expect(() => + runInNewContext(source, { + require: () => ({ BrowserWindow: class {} }), + }), + ).toThrow(/before initialization/); + expect( + analyzeJavaScriptStaticSource(source).electron.browser_windows[0], + ).toMatchObject({ + web_preferences_status: "dynamic", + web_preferences: [], + }); + }, +); + +it("resolves a hoisted window function called after module initialization", () => { + const source = ` + const { BrowserWindow } = require("electron"); + function createWindow() { + return new BrowserWindow({ webPreferences }); + } + const webPreferences = { nodeIntegration: true }; + createWindow(); + `; + expect( + analyzeJavaScriptStaticSource(source).electron.browser_windows[0], + ).toMatchObject({ + web_preferences_status: "object-literal", + web_preferences: [ + { name: "nodeIntegration", value: { status: "literal", value: true } }, + ], + }); +}); + describe("Electron option evidence", () => { it.each([ '({ webPreferences: { preload: "./preload.js", sandbox: true } } as const)', diff --git a/src/domain/javascript/javascriptSemanticMutationInitializers.ts b/src/domain/javascript/javascriptSemanticMutationInitializers.ts index c7d594c394f..ae486d31f09 100644 --- a/src/domain/javascript/javascriptSemanticMutationInitializers.ts +++ b/src/domain/javascript/javascriptSemanticMutationInitializers.ts @@ -1,4 +1,5 @@ import * as t from "@babel/types"; +import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; import { delayedInstanceFieldValue, @@ -29,6 +30,7 @@ interface StatementPosition { interface BindingReadPosition { readonly position: StatementPosition | null; readonly repeatingLoop: t.Loop | undefined; + readonly enclosingFunction: t.Function | undefined; } interface ExpressionSiblings { @@ -54,6 +56,10 @@ interface InitializerPositionCache { readonly mutation: WeakMap; readonly reference: WeakMap; readonly read: WeakMap; + readonly moduleFunctionReads: WeakMap< + readonly t.Statement[], + WeakMap + >; readonly evaluationChildren: WeakMap< t.Node, ReadonlyMap | null @@ -78,6 +84,7 @@ const initializerPositionCache = ( mutation: new WeakMap(), reference: new WeakMap(), read: new WeakMap(), + moduleFunctionReads: new WeakMap(), evaluationChildren: new WeakMap(), expressionAncestry: undefined, }; @@ -727,7 +734,11 @@ const bindingReadPosition = ( path.push(current); const parent = parents.get(current); if (t.isLoop(parent)) { - result = { position: null, repeatingLoop: parent }; + result = { + position: null, + repeatingLoop: parent, + enclosingFunction: undefined, + }; break; } if ( @@ -735,7 +746,12 @@ const bindingReadPosition = ( (t.isFunction(parent) && !semanticComputedMethodKey(parent, current)) || delayedInstanceFieldValue(parent, current) ) { - result = { position: null, repeatingLoop: undefined }; + result = { + position: null, + repeatingLoop: undefined, + enclosingFunction: + t.isFunction(parent) && current === parent.body ? parent : undefined, + }; break; } current = parent; @@ -798,6 +814,70 @@ type SemanticBindingRead = { readonly binding: JavaScriptSemanticBindingState; }; +/** A hoisted function needs a module-level use after initialization, not a later declaration. */ +const moduleFunctionFirstRead = ( + declaration: t.FunctionDeclaration, + body: readonly t.Statement[], + parents: WeakMap, +): number | null => { + const cache = initializerPositionCache(parents).moduleFunctionReads; + let reads = cache.get(body); + if (reads === undefined) { + reads = new WeakMap(); + cache.set(body, reads); + const functions = new Map(); + for (const statement of body) { + const exported = + t.isExportNamedDeclaration(statement) || + t.isExportDefaultDeclaration(statement); + const node = exported ? statement.declaration : statement; + if (!t.isFunctionDeclaration(node)) continue; + reads.set(node, exported ? null : Infinity); + if (node.id == null) continue; + const name = node.id.name; + if (functions.has(name)) { + const previous = functions.get(name); + if (previous != null) reads.set(previous, null); + reads.set(node, null); + functions.set(name, null); + } else functions.set(name, node); + } + const indexedReads = reads; + const active = new Set(); + for (let index = 0; index < body.length; index++) { + const statement = body[index]; + if (statement === undefined) continue; + traverseJavaScriptAst(statement, { + enter(node, parent) { + if (t.isFunction(node)) active.add(node); + if ( + !t.isIdentifier(node) || + parent === null || + !t.isReferenced(node, parent) + ) + return; + const fn = functions.get(node.name); + if (fn == null || active.has(fn)) return; + const current = indexedReads.get(fn); + if (current === null || current === undefined) return; + // Indirect activations and live exports have no proven module timing. + indexedReads.set( + fn, + active.size > 0 || t.isExportSpecifier(parent) + ? null + : Math.min(current, index), + ); + }, + exit(node) { + if (t.isFunction(node)) active.delete(node); + }, + }); + } + } + const first = reads.get(declaration); + return first !== undefined && Number.isFinite(first) ? first : null; +}; + export const semanticBindingAtRead = ( binding: JavaScriptSemanticBindingState, read: t.Node, @@ -864,7 +944,10 @@ export const semanticBindingAtRead = ( ? null : initializerPosition(reference, parents, true) : initializerPosition(initializer, parents); - const { position, repeatingLoop } = bindingReadPosition(read, parents); + const { position, repeatingLoop, enclosingFunction } = bindingReadPosition( + read, + parents, + ); // Coverage and latest-value selection are independent. A preceding write // can cover entry even when another write's order remains unknown. const hasDefiniteInitializer = @@ -898,26 +981,15 @@ export const semanticBindingAtRead = ( hasDefiniteInitializer, }; const functionStatementPosition = (): StatementPosition | null => { - let child: t.Node = read; + if (enclosingFunction === undefined) return null; + if (t.isStatement(enclosingFunction)) + return statementPosition(enclosingFunction, parents); for ( - let parent = parents.get(child); - parent !== undefined; - child = parent, parent = parents.get(parent) - ) { - if ( - t.isFunction(parent) && - (child === parent.body || semanticComputedMethodKey(parent, child)) - ) { - if (t.isStatement(parent)) return statementPosition(parent, parents); - for ( - let owner = parents.get(parent); - owner !== undefined; - owner = parents.get(owner) - ) - if (t.isStatement(owner)) return statementPosition(owner, parents); - return null; - } - } + let owner = parents.get(enclosingFunction); + owner !== undefined; + owner = parents.get(owner) + ) + if (t.isStatement(owner)) return statementPosition(owner, parents); return null; }; const moduleConstBinding = @@ -928,18 +1000,28 @@ export const semanticBindingAtRead = ( const functionPosition = moduleConstBinding ? functionStatementPosition() : null; - const moduleConstInitializerIsBeforeFunction = + const firstFunctionRead = + moduleConstBinding && + sourcePosition !== null && + functionPosition !== null && + sourcePosition.body === functionPosition.body && + t.isFunctionDeclaration(enclosingFunction) + ? moduleFunctionFirstRead(enclosingFunction, sourcePosition.body, parents) + : null; + const moduleConstInitializedBeforeActivation = moduleConstBinding && sourcePosition !== null && functionPosition !== null && sourcePosition.body === functionPosition.body && - positionPrecedes(sourcePosition, functionPosition); + (t.isFunctionDeclaration(enclosingFunction) + ? firstFunctionRead !== null && sourcePosition.index < firstFunctionRead + : positionPrecedes(sourcePosition, functionPosition)); const uncertainOrder = repeatingLoop || sourcePosition === null || position === null || sourcePosition.body !== position.body; - if (uncertainOrder && moduleConstInitializerIsBeforeFunction) + if (uncertainOrder && moduleConstInitializedBeforeActivation) // Initialization coverage does not order a capture in another activation. return { status: "unresolved", binding, hasDefiniteInitializer: true }; if (uncertainOrder) diff --git a/tests/acceptance/applications/electronModuleInitializationCli.test.ts b/tests/acceptance/applications/electronModuleInitializationCli.test.ts new file mode 100644 index 00000000000..a2b1345b39e --- /dev/null +++ b/tests/acceptance/applications/electronModuleInitializationCli.test.ts @@ -0,0 +1,61 @@ +import { expect } from "vitest"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; +import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js"; +import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js"; + +cliTest.for(["CLI", "stdio MCP"] as const)( + "preserves hoisted Electron initialization uncertainty through %s", + async (surface, { cli }) => { + const root = await createTestTempDirectory("rea-electron-initialization-"); + await writeFixtureFiles(root, { + "package.json": JSON.stringify({ main: "early.cjs" }), + "early.cjs": ` + const { BrowserWindow } = require("electron"); + createWindow(); + const webPreferences = { nodeIntegration: true }; + function createWindow() { + return new BrowserWindow({ webPreferences }); + } + require("./late.cjs"); + throw new Error("must not execute source"); + `, + "late.cjs": ` + const { BrowserWindow } = require("electron"); + function createWindow() { + return new BrowserWindow({ webPreferences }); + } + const webPreferences = { nodeIntegration: true }; + createWindow(); + throw new Error("must not execute source"); + `, + }); + const result = await analyzeJavaScriptDirectory(surface, cli, root); + expect(result.statistics.parse_failures).toBe(0); + const windows = result.graph.nodes.filter( + ({ kind }) => kind === "browser-window", + ); + expect(windows).toHaveLength(2); + const observations = windows.flatMap(({ observations }) => observations); + const inSource = (source: string) => + observations.filter( + ({ evidence }) => + evidence.location.available && + evidence.location.value.kind === "source-range" && + evidence.location.value.source === source, + ); + expect(inSource("early.cjs")).toHaveLength(1); + expect(inSource("early.cjs")[0]?.properties).toMatchObject({ + web_preferences_status: "dynamic", + web_preferences: [], + }); + expect(inSource("late.cjs")).toHaveLength(1); + expect(inSource("late.cjs")[0]?.properties).toMatchObject({ + web_preferences_status: "object-literal", + web_preferences: [ + { name: "nodeIntegration", value: { status: "literal", value: true } }, + ], + }); + }, +); From cc2bb1242a5611527bfb99935bc7dbcf85e902f7 Mon Sep 17 00:00:00 2001 From: ShuaixinHuang <1836656514@qq.com> Date: Fri, 30 Oct 2026 23:53:56 +0800 Subject: [PATCH 5/5] fix(javascript): retain function ownership through loops --- .../electronStaticAnalysisOptions.test.ts | 40 ++++++++++++ .../javascriptSemanticMutationInitializers.ts | 63 ++++++++++++------- .../electronModuleInitializationCli.test.ts | 53 ++++++++++++---- 3 files changed, 122 insertions(+), 34 deletions(-) diff --git a/src/domain/javascript/electronStaticAnalysisOptions.test.ts b/src/domain/javascript/electronStaticAnalysisOptions.test.ts index 69623c0bb99..9cd76e22f7c 100644 --- a/src/domain/javascript/electronStaticAnalysisOptions.test.ts +++ b/src/domain/javascript/electronStaticAnalysisOptions.test.ts @@ -295,6 +295,46 @@ it("resolves a hoisted window function called after module initialization", () = }); }); +it.each([ + "for (const item of [0])", + "for (let index = 0; index < 1; index++)", + "while (true)", +])( + "keeps function initialization coverage separate from %s ordering", + (loop) => { + const source = (early: boolean) => ` + const { BrowserWindow } = require("electron"); + ${early ? "createWindow();" : ""} + const webPreferences = { nodeIntegration: true }; + function createWindow() { + ${loop} { + return new BrowserWindow({ webPreferences }); + } + } + ${early ? "" : "createWindow();"} + `; + const runtime = () => ({ require: () => ({ BrowserWindow: class {} }) }); + expect(() => runInNewContext(source(false), runtime())).not.toThrow(); + expect( + analyzeJavaScriptStaticSource(source(false)).electron.browser_windows[0], + ).toMatchObject({ + web_preferences_status: "object-literal", + web_preferences: [ + { name: "nodeIntegration", value: { status: "literal", value: true } }, + ], + }); + expect(() => runInNewContext(source(true), runtime())).toThrow( + /before initialization/, + ); + expect( + analyzeJavaScriptStaticSource(source(true)).electron.browser_windows[0], + ).toMatchObject({ + web_preferences_status: "dynamic", + web_preferences: [], + }); + }, +); + describe("Electron option evidence", () => { it.each([ '({ webPreferences: { preload: "./preload.js", sandbox: true } } as const)', diff --git a/src/domain/javascript/javascriptSemanticMutationInitializers.ts b/src/domain/javascript/javascriptSemanticMutationInitializers.ts index ae486d31f09..9210fe87a3f 100644 --- a/src/domain/javascript/javascriptSemanticMutationInitializers.ts +++ b/src/domain/javascript/javascriptSemanticMutationInitializers.ts @@ -30,7 +30,6 @@ interface StatementPosition { interface BindingReadPosition { readonly position: StatementPosition | null; readonly repeatingLoop: t.Loop | undefined; - readonly enclosingFunction: t.Function | undefined; } interface ExpressionSiblings { @@ -56,6 +55,7 @@ interface InitializerPositionCache { readonly mutation: WeakMap; readonly reference: WeakMap; readonly read: WeakMap; + readonly enclosingFunction: WeakMap; readonly moduleFunctionReads: WeakMap< readonly t.Statement[], WeakMap @@ -84,6 +84,7 @@ const initializerPositionCache = ( mutation: new WeakMap(), reference: new WeakMap(), read: new WeakMap(), + enclosingFunction: new WeakMap(), moduleFunctionReads: new WeakMap(), evaluationChildren: new WeakMap(), expressionAncestry: undefined, @@ -716,6 +717,35 @@ export const semanticMutationInitializers = ( }; }; +/** Function ownership is independent of uncertain ordering within a loop. */ +const bindingEnclosingFunction = ( + read: t.Node, + parents: WeakMap, +): t.Function | undefined => { + const cache = initializerPositionCache(parents).enclosingFunction; + const path: t.Node[] = []; + let current = read; + let result: t.Function | null = null; + while (true) { + const cached = cache.get(current); + if (cached !== undefined) { + result = cached; + break; + } + path.push(current); + const parent = parents.get(current); + if (parent === undefined || delayedInstanceFieldValue(parent, current)) + break; + if (t.isFunction(parent) && !semanticComputedMethodKey(parent, current)) { + if (current === parent.body) result = parent; + break; + } + current = parent; + } + for (const node of path) cache.set(node, result); + return result ?? undefined; +}; + /** Reuse immutable ancestry facts across reads nested in the same expression. */ const bindingReadPosition = ( read: t.Node, @@ -734,11 +764,7 @@ const bindingReadPosition = ( path.push(current); const parent = parents.get(current); if (t.isLoop(parent)) { - result = { - position: null, - repeatingLoop: parent, - enclosingFunction: undefined, - }; + result = { position: null, repeatingLoop: parent }; break; } if ( @@ -746,12 +772,7 @@ const bindingReadPosition = ( (t.isFunction(parent) && !semanticComputedMethodKey(parent, current)) || delayedInstanceFieldValue(parent, current) ) { - result = { - position: null, - repeatingLoop: undefined, - enclosingFunction: - t.isFunction(parent) && current === parent.body ? parent : undefined, - }; + result = { position: null, repeatingLoop: undefined }; break; } current = parent; @@ -944,10 +965,7 @@ export const semanticBindingAtRead = ( ? null : initializerPosition(reference, parents, true) : initializerPosition(initializer, parents); - const { position, repeatingLoop, enclosingFunction } = bindingReadPosition( - read, - parents, - ); + const { position, repeatingLoop } = bindingReadPosition(read, parents); // Coverage and latest-value selection are independent. A preceding write // can cover entry even when another write's order remains unknown. const hasDefiniteInitializer = @@ -980,6 +998,14 @@ export const semanticBindingAtRead = ( binding, hasDefiniteInitializer, }; + const moduleConstBinding = + binding.scopeId.startsWith("scope:program:") && + binding.declarationKinds.size === 1 && + binding.declarationKinds.has("const") && + !binding.mutable; + const enclosingFunction = moduleConstBinding + ? bindingEnclosingFunction(read, parents) + : undefined; const functionStatementPosition = (): StatementPosition | null => { if (enclosingFunction === undefined) return null; if (t.isStatement(enclosingFunction)) @@ -992,11 +1018,6 @@ export const semanticBindingAtRead = ( if (t.isStatement(owner)) return statementPosition(owner, parents); return null; }; - const moduleConstBinding = - binding.scopeId.startsWith("scope:program:") && - binding.declarationKinds.size === 1 && - binding.declarationKinds.has("const") && - !binding.mutable; const functionPosition = moduleConstBinding ? functionStatementPosition() : null; diff --git a/tests/acceptance/applications/electronModuleInitializationCli.test.ts b/tests/acceptance/applications/electronModuleInitializationCli.test.ts index a2b1345b39e..8bf380c159a 100644 --- a/tests/acceptance/applications/electronModuleInitializationCli.test.ts +++ b/tests/acceptance/applications/electronModuleInitializationCli.test.ts @@ -19,6 +19,8 @@ cliTest.for(["CLI", "stdio MCP"] as const)( return new BrowserWindow({ webPreferences }); } require("./late.cjs"); + require("./early-loop.cjs"); + require("./late-loop.cjs"); throw new Error("must not execute source"); `, "late.cjs": ` @@ -30,13 +32,31 @@ cliTest.for(["CLI", "stdio MCP"] as const)( createWindow(); throw new Error("must not execute source"); `, + "early-loop.cjs": ` + const { BrowserWindow } = require("electron"); + createWindow(); + const webPreferences = { nodeIntegration: true }; + function createWindow() { + for (const item of [0]) return new BrowserWindow({ webPreferences }); + } + throw new Error("must not execute source"); + `, + "late-loop.cjs": ` + const { BrowserWindow } = require("electron"); + const webPreferences = { nodeIntegration: true }; + function createWindow() { + for (const item of [0]) return new BrowserWindow({ webPreferences }); + } + createWindow(); + throw new Error("must not execute source"); + `, }); const result = await analyzeJavaScriptDirectory(surface, cli, root); expect(result.statistics.parse_failures).toBe(0); const windows = result.graph.nodes.filter( ({ kind }) => kind === "browser-window", ); - expect(windows).toHaveLength(2); + expect(windows).toHaveLength(4); const observations = windows.flatMap(({ observations }) => observations); const inSource = (source: string) => observations.filter( @@ -45,17 +65,24 @@ cliTest.for(["CLI", "stdio MCP"] as const)( evidence.location.value.kind === "source-range" && evidence.location.value.source === source, ); - expect(inSource("early.cjs")).toHaveLength(1); - expect(inSource("early.cjs")[0]?.properties).toMatchObject({ - web_preferences_status: "dynamic", - web_preferences: [], - }); - expect(inSource("late.cjs")).toHaveLength(1); - expect(inSource("late.cjs")[0]?.properties).toMatchObject({ - web_preferences_status: "object-literal", - web_preferences: [ - { name: "nodeIntegration", value: { status: "literal", value: true } }, - ], - }); + for (const source of ["early.cjs", "early-loop.cjs"]) { + expect(inSource(source)).toHaveLength(1); + expect(inSource(source)[0]?.properties).toMatchObject({ + web_preferences_status: "dynamic", + web_preferences: [], + }); + } + for (const source of ["late.cjs", "late-loop.cjs"]) { + expect(inSource(source)).toHaveLength(1); + expect(inSource(source)[0]?.properties).toMatchObject({ + web_preferences_status: "object-literal", + web_preferences: [ + { + name: "nodeIntegration", + value: { status: "literal", value: true }, + }, + ], + }); + } }, );