mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
* fix(electron): model view constructors as window-like surfaces Electron's static pass recognized a window only through the literal `BrowserWindow` constructor, so `BaseWindow`, `BrowserView` and `WebContentsView` produced no node and their literal `webPreferences` were invisible. IPC registrations reached through such a view's own `webContents.ipc` were dropped too, because the receiver resolver had the same single-constructor gate -- VS Code alone registers seven channels this way, and a shipped sample of four applications put 29 percent of window construction sites on an unmodeled constructor. Read webPreferences through one constructor set shared by the construction inspector and the IPC receiver resolver. The finding only carries the new `window_constructor` field when the site is not `BrowserWindow`, so existing projections and their evidence stay byte-identical. A non-BrowserWindow owner is not admitted for `loadFile`/`loadURL` on itself: only BrowserWindow exposes those directly. * fix(electron): keep the view constructor label lookup nullable Observation labels are nullable in the application graph schema, so the byLabel lookup failed tsc with TS18047 and took the Static checks job down with it. --------- Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
251 lines
8.5 KiB
TypeScript
251 lines
8.5 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptStaticSource as analyzeUnknownHostSource } from "./javascriptStaticAnalysis.js";
|
|
import { analyzeCommonJsStaticFixture as analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
|
|
|
|
it("retains an unknown-host Electron request as a candidate without claiming IPC identity", () => {
|
|
const analysis = analyzeUnknownHostSource(
|
|
'const { BrowserWindow } = require("electron"); new BrowserWindow().webContents.ipc.handle("x", () => {});',
|
|
);
|
|
expect(analysis.electron.ipc).toEqual([
|
|
expect.objectContaining({
|
|
channel: "x",
|
|
operation: "handle",
|
|
loader_authority: "candidate",
|
|
}),
|
|
]);
|
|
expect(analysis.analysis_coverage?.applicability.electron).toBe("unknown");
|
|
expect(analysis.references).toContainEqual(
|
|
expect.objectContaining({
|
|
specifier: "electron",
|
|
loader_authority: "candidate",
|
|
}),
|
|
);
|
|
expect(
|
|
analysis.analysis_coverage?.semantics.unknown_findings,
|
|
).toBeGreaterThan(0);
|
|
});
|
|
|
|
it.each([
|
|
["webContents", "window.webContents.ipc"],
|
|
["main frame", "window.webContents.mainFrame.ipc"],
|
|
] as const)(
|
|
"retains a proven %s IPC handler beside the global control",
|
|
(_, receiver) => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
const { BrowserWindow, ipcMain } = require("electron");
|
|
const window = new BrowserWindow();
|
|
ipcMain.handle("global-control", () => "global");
|
|
${receiver}.handle("scoped-channel", () => "scoped");
|
|
`);
|
|
expect(analysis.parse_error_count).toBe(0);
|
|
expect(analysis.electron.browser_windows).toHaveLength(1);
|
|
expect(analysis.electron.ipc).toContainEqual(
|
|
expect.objectContaining({
|
|
side: "main",
|
|
operation: "handle",
|
|
channel: "global-control",
|
|
handler_kind: "inline-function",
|
|
}),
|
|
);
|
|
expect(analysis.electron.ipc).toContainEqual(
|
|
expect.objectContaining({
|
|
side: "main",
|
|
operation: "handle",
|
|
channel: "scoped-channel",
|
|
handler_kind: "inline-function",
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
|
|
it("does not label lookalike or shadowed receivers as Electron IPC", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
const { BrowserWindow, ipcMain } = require("electron");
|
|
ipcMain.handle("global-control", () => "global");
|
|
const fake = { webContents: { ipc: { handle() {} } } };
|
|
fake.webContents.ipc.handle("fake", () => {});
|
|
function shadow(BrowserWindow) {
|
|
const window = new BrowserWindow();
|
|
window.webContents.ipc.handle("shadowed", () => {});
|
|
}
|
|
`);
|
|
expect(analysis.parse_error_count).toBe(0);
|
|
expect(analysis.electron.ipc.map(({ channel }) => channel)).toEqual([
|
|
"global-control",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
'const contents = window.webContents; contents.ipc.handle("same", () => {});',
|
|
"web-contents",
|
|
],
|
|
[
|
|
'const frame = window.webContents.mainFrame; frame.ipc.handle("same", () => {});',
|
|
"main-frame",
|
|
],
|
|
[
|
|
'const contents = window.webContents; const frame = contents.mainFrame; const ipc = frame.ipc; ipc.handle("same", () => {});',
|
|
"main-frame",
|
|
],
|
|
])("retains a proven scoped alias: %s", (call, kind) => {
|
|
const findings =
|
|
analyzeJavaScriptStaticSource(`const { BrowserWindow } = require("electron");
|
|
const window = new BrowserWindow(); ${call}`).electron.ipc;
|
|
expect(findings).toHaveLength(1);
|
|
expect(findings[0]?.receiver).toEqual({
|
|
kind,
|
|
owner_location: {
|
|
start: { line: 2, column: 19 },
|
|
end: { line: 2, column: 38 },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("keeps the shared owner's conservative omission after an aliased receiver call", () => {
|
|
const findings =
|
|
analyzeJavaScriptStaticSource(`const { BrowserWindow } = require("electron");
|
|
const window = new BrowserWindow();
|
|
const ipc = window.webContents.mainFrame.ipc;
|
|
ipc.handle("first", () => {});
|
|
window.webContents.mainFrame.ipc.on("after-escape", () => {});`).electron
|
|
.ipc;
|
|
expect(findings.map(({ channel }) => channel)).toEqual(["first"]);
|
|
});
|
|
|
|
it.each([
|
|
"window.webContents = replacement;",
|
|
"window.webContents.ipc = replacement;",
|
|
"window.webContents.ipc.handle = replacement;",
|
|
"const alias = window; alias.webContents.ipc.handle = replacement;",
|
|
"const contents = window.webContents; contents.ipc = replacement;",
|
|
"BrowserWindow.prototype.webContents = replacement;",
|
|
])("omits a mutated receiver: %s", (mutation) => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
const { BrowserWindow } = require("electron");
|
|
const window = new BrowserWindow();
|
|
${mutation}
|
|
window.webContents.ipc.handle("changed", () => {});
|
|
`);
|
|
expect(analysis.parse_error_count).toBe(0);
|
|
expect(analysis.electron.ipc).toEqual([]);
|
|
});
|
|
|
|
it.each(["webContents.ipc", "webContents.mainFrame.ipc"])(
|
|
"does not split one dotted member key into receiver segments: %s",
|
|
(key) => {
|
|
expect(
|
|
analyzeJavaScriptStaticSource(`const {BrowserWindow} = require("electron");
|
|
const window = new BrowserWindow(); window[${JSON.stringify(key)}].handle("fake", () => {});`)
|
|
.electron.ipc,
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["BrowserView", "webContents.ipc", "web-contents"],
|
|
["WebContentsView", "webContents.mainFrame.ipc", "main-frame"],
|
|
] as const)(
|
|
"retains a proven %s %s handler through the view's own contents",
|
|
(constructor, surface, kind) => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
const { ${constructor} } = require("electron");
|
|
const owner = new ${constructor}({});
|
|
owner.${surface}.handle("view-channel", () => {});
|
|
`);
|
|
expect(analysis.parse_error_count).toBe(0);
|
|
expect(analysis.electron.browser_windows).toMatchObject([
|
|
{ window_constructor: constructor, options_status: "object-literal" },
|
|
]);
|
|
expect(analysis.electron.ipc).toMatchObject([
|
|
{
|
|
side: "main",
|
|
operation: "handle",
|
|
channel: "view-channel",
|
|
receiver: { kind },
|
|
},
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each(["webContents.ipc", "webContents.mainFrame.ipc", "loadURL"])(
|
|
"omits a BaseWindow %s receiver that the runtime does not own",
|
|
(surface) => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
const { BaseWindow } = require("electron");
|
|
const base = new BaseWindow({ width: 800 });
|
|
base.${surface}.handle("inert", () => {});
|
|
`);
|
|
expect(analysis.parse_error_count).toBe(0);
|
|
expect(analysis.electron.ipc).toEqual([]);
|
|
expect(analysis.electron.browser_windows).toMatchObject([
|
|
{
|
|
window_constructor: "BaseWindow",
|
|
options_status: "object-literal",
|
|
web_preferences_status: "missing",
|
|
preload_path: null,
|
|
},
|
|
]);
|
|
},
|
|
);
|
|
|
|
it("keeps a lookalike construction unproven for a view constructor", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
const { WebContentsView } = require("electron");
|
|
function shadow(WebContentsView) {
|
|
const view = new WebContentsView({ webPreferences: { sandbox: true } });
|
|
view.webContents.ipc.handle("shadowed", () => {});
|
|
}
|
|
`);
|
|
expect(analysis.parse_error_count).toBe(0);
|
|
expect(analysis.electron.browser_windows).toEqual([]);
|
|
expect(analysis.electron.ipc).toEqual([]);
|
|
});
|
|
|
|
it.each(["BrowserView", "WebContentsView"])(
|
|
"reads literal webPreferences on a %s construction",
|
|
(constructor) => {
|
|
expect(
|
|
analyzeJavaScriptStaticSource(`
|
|
const { ${constructor} } = require("electron");
|
|
const owner = new ${constructor}({ webPreferences: { preload: "./p.js", sandbox: true } });
|
|
`).electron.browser_windows,
|
|
).toMatchObject([
|
|
{
|
|
window_constructor: constructor,
|
|
web_preferences_status: "object-literal",
|
|
web_preferences: [
|
|
{ name: "preload", value: { status: "literal", value: "./p.js" } },
|
|
{ name: "sandbox", value: { status: "literal", value: true } },
|
|
],
|
|
preload_path: "./p.js",
|
|
preload_resolution_context: "module-specifier",
|
|
},
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each(["BaseWindow"])(
|
|
"does not report a webPreferences surface a %s never accepts",
|
|
(constructor) => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
const { ${constructor} } = require("electron");
|
|
const owner = new ${constructor}({ webPreferences: { preload: "./p.js", sandbox: true } });
|
|
`);
|
|
expect(analysis.parse_error_count).toBe(0);
|
|
expect(analysis.electron.browser_windows).toMatchObject([
|
|
{
|
|
window_constructor: constructor,
|
|
options_status: "object-literal",
|
|
web_preferences_status: "missing",
|
|
web_preferences: [],
|
|
preload_path: null,
|
|
},
|
|
]);
|
|
expect(
|
|
analysis.role_paths.filter(({ role }) => role === "preload"),
|
|
).toEqual([]);
|
|
},
|
|
);
|