Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions src/dataflow/cfg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,13 @@ function collectStatementNodes(node: Node, out: Node[]): void {
out.push(s);
for (const clause of s.getClauses()) for (const cs of clause.getStatements()) collectStatementNodes(cs, out);
} else if (Node.isTryStatement(s)) {
const beforeTry = out.length;
collectStatementNodes(s.getTryBlock(), out);
const tryHasNodes = out.length > beforeTry;
const cc = s.getCatchClause();
if (cc) {
// A catch after a structurally empty try is unreachable. Omitting that dead region keeps
// normal flow on the statement list and preserves the graph's all-nodes-reachable contract.
if (cc && tryHasNodes) {
out.push(cc);
collectStatementNodes(cc.getBlock(), out);
}
Expand Down Expand Up @@ -377,15 +381,17 @@ class Lowerer {
const exits: Dangling[] = [];
let catchEntry: number | null = null;
if (cc) {
const catchId = this.idOf.get(cc) as number; // binds the exception variable (a def, stage 3)
catchEntry = catchId;
const catchCtx: LowerCtx = { ...ctx, exceptionTarget: afterCatchTarget };
const catchBody = this.statements(cc.getBlock().getStatements(), catchCtx);
if (catchBody.entry !== null) {
this.addEdge(catchId, catchBody.entry, "fallthrough");
this.routeThroughFinally(catchBody.exits, finLowered, exits);
} else {
this.routeThroughFinally([{ from: catchId, kind: "fallthrough" }], finLowered, exits);
const catchId = this.idOf.get(cc);
if (catchId !== undefined) {
catchEntry = catchId; // binds the exception variable (a def, stage 3)
const catchCtx: LowerCtx = { ...ctx, exceptionTarget: afterCatchTarget };
const catchBody = this.statements(cc.getBlock().getStatements(), catchCtx);
if (catchBody.entry !== null) {
this.addEdge(catchId, catchBody.entry, "fallthrough");
this.routeThroughFinally(catchBody.exits, finLowered, exits);
} else {
this.routeThroughFinally([{ from: catchId, kind: "fallthrough" }], finLowered, exits);
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions test/dataflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ describe("CFG gate", () => {
expect(e).toContainEqual({ source: 6, target: 8, kind: "exception" }); // finally → outward (EXIT)
});

test("an empty try falls through instead of entering its unreachable catch", () => {
const cfg = cfgOf("src/flow.emptyTryCatch");
expect(cfg.nodes.map((node) => node.kind)).toEqual(["entry", "statement", "exit"]);
expect(cfg.edges).toEqual([
{ source: 0, target: 1, kind: "fallthrough" },
{ source: 1, target: 2, kind: "return" },
]);
});

test("throw with no handler edges to EXIT (parse)", () => {
const cfg = cfgOf("src/flow.parse");
const exit = cfg.nodes.length - 1;
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/dataflow-app/src/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,11 @@ export function shadow(): number {
}
return x;
}

export function emptyTryCatch(): number {
try {
} catch {
return 1;
}
return 0;
}