fix(javascript): retain guards after labelled breaks (#7841)

* fix(javascript): retain guards after labelled breaks

* fix(javascript): bound labelled exit inspection

* fix(javascript): preserve local break completion

* fix(javascript): avoid exits through unknown control flow

* fix(javascript): retain guards after bounded jump scan
This commit is contained in:
morluto authored and GitHub committed 2026-10-29 05:09:45 +08:00
1 parent 1e088c19ad
commit 4e663beeb8
6 files changed
+363 -67

No files matched your search

@@ -1,5 +1,4 @@
import * as t from "@babel/types";
import { symbolicDefiniteExit } from "./javascriptSymbolicControlFlow.js";
/** Syntax alone can establish an exit, but never a branch's contextual truth. */
export type ElectronStatementFallthrough =
@@ -11,6 +10,35 @@ export type ElectronStatementFallthrough =
readonly alternateExits: boolean;
};
interface Completion {
readonly fallthrough: boolean;
readonly exits: boolean;
readonly breaks: ReadonlySet<string>;
/** Unsupported control flow may leave the current labelled statement. */
readonly uncertain: boolean;
}
const FALLTHROUGH: Completion = {
fallthrough: true,
exits: false,
breaks: new Set(),
uncertain: false,
};
const EXIT: Completion = {
fallthrough: false,
exits: true,
breaks: new Set(),
uncertain: false,
};
const UNKNOWN: Completion = { ...FALLTHROUGH, uncertain: true };
const mergeCompletions = (left: Completion, right: Completion): Completion => ({
fallthrough: left.fallthrough || right.fallthrough,
exits: left.exits || right.exits,
breaks: new Set([...left.breaks, ...right.breaks]),
uncertain: left.uncertain || right.uncertain,
});
interface PrefixEntry {
readonly position: number;
readonly fact: ElectronStatementFallthrough;
@@ -34,23 +62,161 @@ export class ElectronCallPathBlockPrefixes {
ElectronStatementFallthrough | null
>();
constructor(private readonly admitVisit: () => boolean) {}
constructor(
private readonly admitVisit: () => boolean,
private readonly parents?: WeakMap<t.Node, t.Node>,
) {}
statement(statement: t.Statement): ElectronStatementFallthrough | null {
/** Retain jumps that may leave an unsupported construct, without entering functions. */
private possibleJumps(root: t.Node): Completion | undefined {
const breaks = new Set<string>();
let uncertain = false;
const pending: Array<{ node: t.Node; depth: number }> = [
{ node: root, depth: 0 },
];
while (pending.length > 0) {
const item = pending.pop();
if (item === undefined) break;
const { node, depth } = item;
if (!this.admitVisit()) return undefined;
if (depth >= 128) {
uncertain = true;
continue;
}
if (t.isFunction(node) || t.isClass(node)) continue;
if (
(t.isBreakStatement(node) || t.isContinueStatement(node)) &&
node.label
) {
// Labels inside this unsupported construct consume their own jumps.
let parent = this.parents?.get(node);
let local = false;
while (parent && parent !== root) {
if (!this.admitVisit()) return undefined;
if (
t.isLabeledStatement(parent) &&
parent.label.name === node.label.name
) {
local = true;
break;
}
parent = this.parents?.get(parent);
}
if (!local) breaks.add(node.label.name);
}
for (const key of t.VISITOR_KEYS[node.type] ?? []) {
const value = (node as unknown as Record<string, unknown>)[key];
const children = Array.isArray(value) ? value : [value];
for (const child of children) {
if (t.isNode(child)) pending.push({ node: child, depth: depth + 1 });
}
}
}
return { ...FALLTHROUGH, breaks, uncertain };
}
/** Track local breaks separately so unreachable statements cannot invent an exit. */
private completion(
node: t.Node | null | undefined,
depth = 0,
): Completion | undefined {
if (!node) return FALLTHROUGH;
if (depth >= 128) return UNKNOWN;
if (!this.admitVisit()) return undefined;
if (t.isReturnStatement(node) || t.isThrowStatement(node)) return EXIT;
if (t.isBreakStatement(node))
return {
fallthrough: false,
exits: false,
breaks: new Set([node.label?.name ?? ""]),
uncertain: false,
};
if (t.isLabeledStatement(node)) {
const body = this.completion(node.body, depth + 1);
if (body === undefined || !body.breaks.has(node.label.name)) return body;
const breaks = new Set(body.breaks);
breaks.delete(node.label.name);
return { ...body, fallthrough: true, breaks };
}
if (t.isBlockStatement(node)) {
let result = FALLTHROUGH;
for (const child of node.body) {
if (!result.fallthrough) break;
const next = this.completion(child, depth + 1);
if (next === undefined) return undefined;
result = mergeCompletions({ ...result, fallthrough: false }, next);
}
return result;
}
if (
t.isLoop(node) ||
t.isSwitchStatement(node) ||
t.isTryStatement(node) ||
t.isWithStatement(node)
)
return this.possibleJumps(node);
if (!t.isIfStatement(node)) return FALLTHROUGH;
const consequent = this.completion(node.consequent, depth + 1);
if (consequent === undefined) return undefined;
const alternate = this.completion(node.alternate, depth + 1);
return alternate === undefined
? undefined
: mergeCompletions(consequent, alternate);
}
private exits(
node: t.Node | null | undefined,
block: t.BlockStatement,
): 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;
let matched = false;
for (
let parent = this.parents.get(block), n = 0;
parent && n < 128;
parent = this.parents.get(parent), n++
) {
if (!this.admitVisit()) return undefined;
if (t.isFunction(parent)) break;
if (t.isLabeledStatement(parent) && parent.label.name === label) {
matched = true;
break;
}
}
if (!matched) return false;
}
return result.exits || result.breaks.size > 0;
}
statement(
statement: t.Statement,
block: t.BlockStatement,
): ElectronStatementFallthrough | null | undefined {
const cached = this.#facts.get(statement);
if (cached !== undefined) return cached;
const fact: ElectronStatementFallthrough | null = symbolicDefiniteExit(
statement,
)
? { kind: "exit" }
: t.isIfStatement(statement)
? {
kind: "branch",
test: statement.test,
consequentExits: symbolicDefiniteExit(statement.consequent),
alternateExits: symbolicDefiniteExit(statement.alternate),
}
: null;
let fact: ElectronStatementFallthrough | null;
if (t.isIfStatement(statement)) {
if (!this.admitVisit()) return undefined;
const consequent = this.exits(statement.consequent, block);
const alternate = this.exits(statement.alternate, block);
if (consequent === undefined || alternate === undefined) return undefined;
fact =
consequent && alternate
? { kind: "exit" }
: {
kind: "branch",
test: statement.test,
consequentExits: consequent,
alternateExits: alternate,
};
} else {
const result = this.exits(statement, block);
if (result === undefined) return undefined;
fact = result ? { kind: "exit" } : null;
}
this.#facts.set(statement, fact);
return fact;
}
@@ -70,7 +236,8 @@ export class ElectronCallPathBlockPrefixes {
for (const [position, statement] of block.body.entries()) {
if (!this.admitVisit()) return undefined;
positions.set(statement, position);
const fact = this.statement(statement);
const fact = this.statement(statement, block);
if (fact === undefined) return undefined;
if (fact !== null) entries.push({ position, fact });
if ((position + 1) % 1024 === 0) yield;
}