fix(javascript): preserve initializer execution and lexical ownership

This commit is contained in:
morluto committed 2026-10-24 02:29:44 +08:00
1 parent 5cbcd58550
commit fba4faf88a
8 files changed
+488 -41

No files matched your search

@@ -6,6 +6,8 @@ import {
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { childNodes } from "./javascriptSemanticTraversal.js";
const preserveGrouping = { stopAtParentheses: true } as const;
export type JavaScriptSemanticIterationCardinality =
@@ -155,3 +157,94 @@ const optionalChainMayShortCircuit = (expression: t.Node): boolean => {
else return false;
}
};
interface AbruptCompletionSummary {
readonly breaks: ReadonlySet<string | null>;
readonly continues: ReadonlySet<string | null>;
}
const emptyAbruptLabels: ReadonlySet<string | null> = new Set();
const emptyAbruptSummary: AbruptCompletionSummary = {
breaks: emptyAbruptLabels,
continues: emptyAbruptLabels,
};
const abruptSummaries = new WeakMap<t.Node, AbruptCompletionSummary>();
const abruptPrefixes = new WeakMap<
t.BlockStatement | t.Program,
ReadonlySet<t.Node>
>();
/** Loop and label exits crossing a statement boundary, excluding nested callables. */
const abruptSummary = (root: t.Node): AbruptCompletionSummary => {
const pending: { node: t.Node; visited: boolean }[] = [
{ node: root, visited: false },
];
while (pending.length > 0) {
const { node, visited } = pending.pop()!;
if (abruptSummaries.has(node)) continue;
const children =
t.isFunction(node) || t.isClass(node)
? []
: childNodes(node).filter(
(child) =>
t.isStatement(child) ||
t.isSwitchCase(child) ||
t.isCatchClause(child),
);
if (!visited) {
pending.push({ node, visited: true });
for (const child of children)
if (!abruptSummaries.has(child))
pending.push({ node: child, visited: false });
continue;
}
let breaks: Set<string | null> | undefined,
continues: Set<string | null> | undefined;
if (t.isBreakStatement(node))
(breaks ??= new Set()).add(node.label?.name ?? null);
if (t.isContinueStatement(node))
(continues ??= new Set()).add(node.label?.name ?? null);
for (const child of children) {
const summary = abruptSummaries.get(child)!;
for (const label of summary.breaks) (breaks ??= new Set()).add(label);
for (const label of summary.continues)
(continues ??= new Set()).add(label);
}
if (t.isLoop(node) || t.isSwitchStatement(node)) breaks?.delete(null);
if (t.isLoop(node)) continues?.delete(null);
if (t.isLabeledStatement(node)) {
breaks?.delete(node.label.name);
continues?.delete(node.label.name);
}
abruptSummaries.set(
node,
(breaks?.size ?? 0) > 0 || (continues?.size ?? 0) > 0
? {
breaks: breaks ?? emptyAbruptLabels,
continues: continues ?? emptyAbruptLabels,
}
: emptyAbruptSummary,
);
}
return abruptSummaries.get(root)!;
};
/** An earlier sibling may leave this body before reaching the selected statement. */
export const semanticPriorStatementMayExit = (
parent: t.Node,
child: t.Node,
): boolean => {
if (!t.isBlockStatement(parent) && !t.isProgram(parent)) return false;
let skipped = abruptPrefixes.get(parent);
if (skipped === undefined) {
const prefixes = new Set<t.Node>();
let exits = false;
for (const statement of parent.body) {
if (exits) prefixes.add(statement);
const summary = abruptSummary(statement);
exits ||= summary.breaks.size > 0 || summary.continues.size > 0;
}
skipped = prefixes;
abruptPrefixes.set(parent, skipped);
}
return skipped.has(child);
};