mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 02:02:36 +00:00
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:
1 parent
827b808248
commit
4073cfc932
3 files changed
+116
-10
No files matched your search
@@ -45,6 +45,9 @@ interface PrefixEntry {
|
||||
readonly fact: ElectronStatementFallthrough;
|
||||
}
|
||||
|
||||
/** Both braced blocks and switch cases own ordered statement sequences. */
|
||||
type StatementList = t.BlockStatement | t.SwitchCase;
|
||||
|
||||
interface BlockIndex {
|
||||
readonly positions: WeakMap<t.Node, number>;
|
||||
readonly entries: readonly PrefixEntry[];
|
||||
@@ -57,7 +60,7 @@ interface BlockIndex {
|
||||
* to the invocation's symbolic environment and can establish unknown coverage.
|
||||
*/
|
||||
export class ElectronCallPathBlockPrefixes {
|
||||
readonly #blocks = new WeakMap<t.BlockStatement, BlockIndex>();
|
||||
readonly #blocks = new WeakMap<StatementList, BlockIndex>();
|
||||
readonly #facts = new WeakMap<
|
||||
t.Statement,
|
||||
ElectronStatementFallthrough | null
|
||||
@@ -120,7 +123,7 @@ export class ElectronCallPathBlockPrefixes {
|
||||
if (depth >= 128) return UNKNOWN;
|
||||
if (!this.admitVisit()) return undefined;
|
||||
if (t.isReturnStatement(node) || t.isThrowStatement(node)) return EXIT;
|
||||
if (t.isBreakStatement(node))
|
||||
if (t.isBreakStatement(node) || t.isContinueStatement(node))
|
||||
return {
|
||||
fallthrough: false,
|
||||
exits: false,
|
||||
@@ -162,13 +165,15 @@ export class ElectronCallPathBlockPrefixes {
|
||||
|
||||
private exits(
|
||||
node: t.Node | null | undefined,
|
||||
block: t.BlockStatement,
|
||||
block: StatementList,
|
||||
): boolean | undefined {
|
||||
const result = this.completion(node);
|
||||
if (result === undefined || result.fallthrough || result.uncertain)
|
||||
return result === undefined ? undefined : false;
|
||||
for (const label of result.breaks) {
|
||||
if (!label || !this.parents) return false;
|
||||
// A switch-local break or enclosing-loop continue ends this case.
|
||||
if (!label && t.isSwitchCase(block)) continue;
|
||||
if (!this.parents) return false;
|
||||
let matched = false;
|
||||
for (
|
||||
let parent = this.parents.get(block), n = 0;
|
||||
@@ -177,7 +182,11 @@ export class ElectronCallPathBlockPrefixes {
|
||||
) {
|
||||
if (!this.admitVisit()) return undefined;
|
||||
if (t.isFunction(parent)) break;
|
||||
if (t.isLabeledStatement(parent) && parent.label.name === label) {
|
||||
if (
|
||||
label
|
||||
? t.isLabeledStatement(parent) && parent.label.name === label
|
||||
: t.isLoop(parent) || t.isSwitchStatement(parent)
|
||||
) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
@@ -189,7 +198,7 @@ export class ElectronCallPathBlockPrefixes {
|
||||
|
||||
statement(
|
||||
statement: t.Statement,
|
||||
block: t.BlockStatement,
|
||||
block: StatementList,
|
||||
): ElectronStatementFallthrough | null | undefined {
|
||||
const cached = this.#facts.get(statement);
|
||||
if (cached !== undefined) return cached;
|
||||
@@ -218,7 +227,7 @@ export class ElectronCallPathBlockPrefixes {
|
||||
}
|
||||
|
||||
*before(
|
||||
block: t.BlockStatement,
|
||||
block: StatementList,
|
||||
child: t.Node,
|
||||
): Generator<
|
||||
void,
|
||||
@@ -229,7 +238,8 @@ export class ElectronCallPathBlockPrefixes {
|
||||
if (index === undefined) {
|
||||
const positions = new WeakMap<t.Node, number>();
|
||||
const entries: PrefixEntry[] = [];
|
||||
for (const [position, statement] of block.body.entries()) {
|
||||
const statements = t.isSwitchCase(block) ? block.consequent : block.body;
|
||||
for (const [position, statement] of statements.entries()) {
|
||||
if (!this.admitVisit()) return undefined;
|
||||
positions.set(statement, position);
|
||||
const fact = this.statement(statement, block);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1458,10 +1458,17 @@ export function* collectElectronWrappedCallPathsSteps(
|
||||
const branch = node.cases[index]!;
|
||||
if (branch.test) walk(branch.test, env, context);
|
||||
if (remainingVisits < 0) return;
|
||||
const branchContext = switchCaseContext(node, index, context);
|
||||
let branchContext = switchCaseContext(node, index, context);
|
||||
for (const statement of branch.consequent) {
|
||||
if (remainingVisits < 0) return;
|
||||
walk(statement, env, branchContext);
|
||||
const fallthrough = sequentialFallthrough(
|
||||
prefixes.statement(statement, branch) ?? null,
|
||||
env,
|
||||
branchContext,
|
||||
);
|
||||
if (!fallthrough.continues) break;
|
||||
branchContext = fallthrough.context;
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -1532,7 +1539,11 @@ export function* collectElectronWrappedCallPathsSteps(
|
||||
parent.left,
|
||||
parent.operator === "??" ? "nullish" : parent.operator === "&&",
|
||||
);
|
||||
} else if (t.isBlockStatement(parent)) {
|
||||
}
|
||||
if (
|
||||
t.isBlockStatement(parent) ||
|
||||
(t.isSwitchCase(parent) && child !== parent.test)
|
||||
) {
|
||||
const prefix = yield* prefixes.before(parent, child);
|
||||
if (remainingVisits < 0) return;
|
||||
if (prefix !== undefined) {
|
||||
|
||||
Reference in new issue
Block a user