fix(electron): honor ordered switch-case completion in call paths (#10057)

fix(electron): honor switch-case completion in wrapper paths
This commit is contained in:
morluto authored and GitHub committed 2026-11-11 17:14:16 +08:00
1 parent 827b808248
commit 4073cfc932
3 files changed
+116 -10

No files matched your search

@@ -1,3 +1,4 @@
import { Script } from "node:vm";
import { describe, expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
@@ -16,6 +17,90 @@ it("retains switch selection on a direct bridge exposure", () => {
]);
});
it.each(
["return", "throw new Error('stop')", "break", "continue"].flatMap(
(completion) =>
[false, true].flatMap((wrapped) =>
[false, true].map((nested) => ({ completion, wrapped, nested })),
),
),
)(
"excludes switch-case calls after $completion (wrapped=$wrapped, nested=$nested)",
({ completion, wrapped, nested }) => {
const exposure = (channel: string) =>
`e.contextBridge.exposeInMainWorld("${channel}", {send:()=>e.ipcRenderer.send("${channel}")});`;
const body = `for (const item of [0]) { switch (mode) {
case "off": ${nested ? "{" : ""} ${completion}; ${wrapped ? 'e.ipcRenderer.send("dead");' : exposure("dead")} ${nested ? "}" : ""}
default: ${wrapped ? 'e.ipcRenderer.send("live");' : exposure("live")}
} }`;
const source = `const e=require("electron");
${wrapped ? `e.contextBridge.exposeInMainWorld("api", {run(){${body}}});` : `function install(){${body}}`}
`;
expect(() => new Script(source)).not.toThrow();
const result = analyzeJavaScriptStaticSource(source);
expect(
result.electron.call_paths?.some(({ channel }) => channel === "dead"),
).toBe(false);
expect(
result.electron.call_paths?.some(({ channel }) => channel === "live"),
).toBe(true);
// Unreachable syntax remains observed; only a derived invocation path is removed.
expect(result.electron.ipc.some(({ channel }) => channel === "dead")).toBe(
true,
);
},
);
it.each([false, true])(
"retains an earlier case-local return guard (wrapped=%s)",
(wrapped) => {
const target = wrapped
? 'e.ipcRenderer.send("guarded");'
: 'e.contextBridge.exposeInMainWorld("api", {send:()=>e.ipcRenderer.send("guarded")});';
const body = `switch(mode){case "selected": if(blocked)return; ${target}}`;
const source = `const e=require("electron");
${wrapped ? `e.contextBridge.exposeInMainWorld("api",{run(){${body}}});` : `function install(){${body}}`}
`;
expect(() => new Script(source)).not.toThrow();
const result = analyzeJavaScriptStaticSource(source);
const paths =
result.electron.call_paths?.filter(
({ channel }) => channel === "guarded",
) ?? [];
expect(paths.length).toBeGreaterThan(0);
expect(
paths.every(({ conditions }) =>
conditions.some(({ expression }) => expression === "!(blocked)"),
),
).toBe(true);
},
);
it.each(
[
"while (true) { break; }",
"switch (inner) { case 0: break; }",
"for (const nested of [0]) { continue; }",
"local: { break local; }",
].flatMap((inner) => [false, true].map((wrapped) => ({ inner, wrapped }))),
)(
"retains outer-case continuation after $inner (wrapped=$wrapped)",
({ inner, wrapped }) => {
const target = wrapped
? 'e.ipcRenderer.send("live");'
: 'e.contextBridge.exposeInMainWorld("api", {send:()=>e.ipcRenderer.send("live")});';
const body = `switch (mode) { case "selected": ${inner} ${target} }`;
const source = `const e=require("electron");
${wrapped ? `e.contextBridge.exposeInMainWorld("api", {run(){${body}}});` : `function install(){${body}}`}
`;
expect(() => new Script(source)).not.toThrow();
const result = analyzeJavaScriptStaticSource(source);
expect(
result.electron.call_paths?.some(({ channel }) => channel === "live"),
).toBe(true);
},
);
it("retains a conservative selection for stacked and fallthrough cases", () => {
const result = analyzeJavaScriptStaticSource(`
const { contextBridge, ipcRenderer } = require("electron");