fix(electron): unwrap transparent inline window options

Use the shared expression owner for BrowserWindow options and webPreferences. Retain overwrite and spread uncertainty through CLI and MCP evidence.

Refs: #4250, #4281

Co-authored-by: Geekwolf <geekwolf@163.com>
This commit is contained in:
morluto
2026-10-16 08:14:51 +08:00
parent 38f92d108e
commit b67baede2e
3 changed files with 165 additions and 7 deletions
@@ -25,7 +25,10 @@ import {
staticPath,
staticPathResolutionContext,
} from "./javascriptStaticAnalysisHelpers.js";
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
import {
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
/** Inspect BrowserWindow, contextBridge, and utility-process syntax. */
@@ -45,7 +48,11 @@ const inspectBrowserWindow = (
): void => {
const path = electronCalleePath(node.callee, context);
if (path?.length !== 1 || path[0] !== "BrowserWindow") return;
const options = argumentNode(node.arguments[0]);
const argument = argumentNode(node.arguments[0]);
const options =
argument === undefined
? undefined
: unwrapJavaScriptExpression(argument).node;
const collected = collectWindowOptions(context.source, options);
context.accumulator.unknownFindings += collected.unknown;
const finding: ElectronBrowserWindowFinding = {
@@ -79,17 +86,18 @@ const collectWindowOptions = (
readonly unknown: number;
} => {
const lookup = objectProperty(options, "webPreferences");
if (
lookup.status !== "explicit" ||
!t.isObjectExpression(lookup.property.value)
)
const preferences =
lookup.status === "explicit"
? unwrapJavaScriptExpression(lookup.property.value).node
: undefined;
if (lookup.status !== "explicit" || !t.isObjectExpression(preferences))
return {
status: lookup.status === "missing" ? "missing" : "dynamic",
preferences: [],
preload: { preload_path: null, preload_resolution_context: null },
unknown: lookup.status === "missing" ? 0 : 1,
};
return collectWebPreferences(source, lookup.property.value);
return collectWebPreferences(source, preferences);
};
const collectWebPreferences = (
@@ -3,6 +3,34 @@ import { describe, expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
describe("Electron option evidence", () => {
it.each([
'({ webPreferences: { preload: "./preload.js", sandbox: true } } as const)',
'{ webPreferences: ({ preload: "./preload.js", sandbox: true } as const) }',
'{ webPreferences: { preload: "./preload.js" as const, sandbox: true } }',
])("unwraps type-only wrappers around inline options: %s", (options) => {
const analysis = analyzeJavaScriptStaticSource(`
import { BrowserWindow } from "electron";
new BrowserWindow(${options});
`);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.electron.browser_windows).toMatchObject([
{
options_status: "object-literal",
web_preferences_status: "object-literal",
preload_path: "./preload.js",
preload_resolution_context: "module-specifier",
web_preferences: [
{
name: "preload",
value: { status: "literal", value: "./preload.js" },
},
{ name: "sandbox", value: { status: "literal", value: true } },
],
},
]);
});
it("keeps the final explicit preload and service name with unrelated keys", () => {
const analysis = analyzeJavaScriptStaticSource(`
const { BrowserWindow, utilityProcess } = require("electron");
@@ -175,3 +175,125 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
}
},
);
cliTest.for(["CLI", "stdio MCP"] as const)(
"preserves asserted inline options and unresolved overrides through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-asserted-window-options-");
const scenarios = [
{
sourceLine:
'new BrowserWindow(({webPreferences:{preload:"./first.js",sandbox:true}} as const));',
expectedPreload: "./first.js",
preferencesStatus: "object-literal",
},
{
sourceLine:
'new BrowserWindow({webPreferences:({preload:"./first.js",sandbox:true} as const)});',
expectedPreload: "./first.js",
preferencesStatus: "object-literal",
},
{
sourceLine:
'new BrowserWindow({webPreferences:{preload:"./first.js" as const,sandbox:true}});',
expectedPreload: "./first.js",
preferencesStatus: "object-literal",
},
{
sourceLine:
"new BrowserWindow(({webPreferences:getPreferences()} as const));",
expectedPreload: null,
preferencesStatus: "dynamic",
},
{
sourceLine:
'new BrowserWindow(({webPreferences:{preload:"./first.js",...runtimeOptions}} as const));',
expectedPreload: null,
preferencesStatus: "object-literal",
},
] as const;
const sourceLines = scenarios.map(({ sourceLine }) => sourceLine);
await Promise.all([
writeFile(
join(root, "main.ts"),
`import { BrowserWindow } from "electron";\n${sourceLines.join("\n")}\n`,
),
writeFile(join(root, "first.js"), 'throw new Error("inert preload");\n'),
]);
let document: unknown;
if (surface === "CLI") {
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
document = response.json;
} else {
const { call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
document = response.structuredContent;
}
const { graph } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(document).normalized_result;
const windows = graph.nodes.filter(({ kind }) => kind === "browser-window");
expect(windows).toHaveLength(sourceLines.length);
for (const [index, scenario] of scenarios.entries()) {
const { sourceLine } = scenario;
const window = windows.find((node) => {
const location = node.observations[0]?.evidence.location;
return (
location?.available &&
location.value.kind === "source-range" &&
location.value.start.line === index + 2
);
});
const observation = window?.observations[0];
const known = scenario.expectedPreload !== null;
expect.soft(observation, sourceLine).toMatchObject({
properties: {
options_status: "object-literal",
web_preferences_status: scenario.preferencesStatus,
preload_path: scenario.expectedPreload,
preload_resolution_context: known ? "module-specifier" : null,
},
evidence: {
state: "observed",
location: {
available: true,
value: {
kind: "source-range",
source: "main.ts",
start: { line: index + 2, column: 0 },
end: { line: index + 2, column: sourceLine.length - 1 },
},
},
},
});
const associations = graph.edges.filter(
(edge) =>
edge.source_node_id === window?.node_id &&
edge.evidence.extractor.operation ===
"associate-browser-window-preload",
);
if (known) {
expect.soft(associations, sourceLine).toMatchObject([
{
relation: "loads",
properties: {
declared_path: "./first.js",
resolved_path: "first.js",
},
},
]);
} else {
expect.soft(associations, sourceLine).toEqual([]);
}
}
},
);