Skip to content

Commit 1c42c1a

Browse files
authored
Fix control flow analysis in try-catch-finally (#34880)
* Revise creation of control flow graph for try-catch-finally statements * Add tests * Accept new baselines
1 parent 95be956 commit 1c42c1a

File tree

5 files changed

+483
-94
lines changed

5 files changed

+483
-94
lines changed

src/compiler/binder.ts

Lines changed: 56 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,6 @@ namespace ts {
167167
return node;
168168
}
169169

170-
let flowNodeCreated: <T extends FlowNode>(node: T) => T = initFlowNode;
171-
172170
const binder = createBinder();
173171

174172
export function bindSourceFile(file: SourceFile, options: CompilerOptions) {
@@ -199,6 +197,7 @@ namespace ts {
199197
let currentReturnTarget: FlowLabel | undefined;
200198
let currentTrueTarget: FlowLabel | undefined;
201199
let currentFalseTarget: FlowLabel | undefined;
200+
let currentExceptionTarget: FlowLabel | undefined;
202201
let preSwitchCaseFlow: FlowNode | undefined;
203202
let activeLabels: ActiveLabel[] | undefined;
204203
let hasExplicitReturn: boolean;
@@ -271,6 +270,7 @@ namespace ts {
271270
currentReturnTarget = undefined;
272271
currentTrueTarget = undefined;
273272
currentFalseTarget = undefined;
273+
currentExceptionTarget = undefined;
274274
activeLabels = undefined!;
275275
hasExplicitReturn = false;
276276
emitFlags = NodeFlags.None;
@@ -624,11 +624,11 @@ namespace ts {
624624
blockScopeContainer.locals = undefined;
625625
}
626626
if (containerFlags & ContainerFlags.IsControlFlowContainer) {
627-
const saveFlowNodeCreated = flowNodeCreated;
628627
const saveCurrentFlow = currentFlow;
629628
const saveBreakTarget = currentBreakTarget;
630629
const saveContinueTarget = currentContinueTarget;
631630
const saveReturnTarget = currentReturnTarget;
631+
const saveExceptionTarget = currentExceptionTarget;
632632
const saveActiveLabels = activeLabels;
633633
const saveHasExplicitReturn = hasExplicitReturn;
634634
const isIIFE = containerFlags & ContainerFlags.IsFunctionExpression && !hasModifier(node, ModifierFlags.Async) &&
@@ -644,11 +644,11 @@ namespace ts {
644644
// We create a return control flow graph for IIFEs and constructors. For constructors
645645
// we use the return control flow graph in strict property initialization checks.
646646
currentReturnTarget = isIIFE || node.kind === SyntaxKind.Constructor ? createBranchLabel() : undefined;
647+
currentExceptionTarget = undefined;
647648
currentBreakTarget = undefined;
648649
currentContinueTarget = undefined;
649650
activeLabels = undefined;
650651
hasExplicitReturn = false;
651-
flowNodeCreated = initFlowNode;
652652
bindChildren(node);
653653
// Reset all reachability check related flags on node (for incremental scenarios)
654654
node.flags &= ~NodeFlags.ReachabilityAndEmitFlags;
@@ -674,9 +674,9 @@ namespace ts {
674674
currentBreakTarget = saveBreakTarget;
675675
currentContinueTarget = saveContinueTarget;
676676
currentReturnTarget = saveReturnTarget;
677+
currentExceptionTarget = saveExceptionTarget;
677678
activeLabels = saveActiveLabels;
678679
hasExplicitReturn = saveHasExplicitReturn;
679-
flowNodeCreated = saveFlowNodeCreated;
680680
}
681681
else if (containerFlags & ContainerFlags.IsInterface) {
682682
seenThisKeyword = false;
@@ -961,27 +961,26 @@ namespace ts {
961961
return antecedent;
962962
}
963963
setFlowNodeReferenced(antecedent);
964-
return flowNodeCreated({ flags, antecedent, node: expression });
964+
return initFlowNode({ flags, antecedent, node: expression });
965965
}
966966

967967
function createFlowSwitchClause(antecedent: FlowNode, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): FlowNode {
968968
setFlowNodeReferenced(antecedent);
969-
return flowNodeCreated({ flags: FlowFlags.SwitchClause, antecedent, switchStatement, clauseStart, clauseEnd });
969+
return initFlowNode({ flags: FlowFlags.SwitchClause, antecedent, switchStatement, clauseStart, clauseEnd });
970970
}
971971

972-
function createFlowAssignment(antecedent: FlowNode, node: Expression | VariableDeclaration | BindingElement): FlowNode {
972+
function createFlowMutation(flags: FlowFlags, antecedent: FlowNode, node: Node): FlowNode {
973973
setFlowNodeReferenced(antecedent);
974-
return flowNodeCreated({ flags: FlowFlags.Assignment, antecedent, node });
974+
const result = initFlowNode({ flags, antecedent, node });
975+
if (currentExceptionTarget) {
976+
addAntecedent(currentExceptionTarget, result);
977+
}
978+
return result;
975979
}
976980

977981
function createFlowCall(antecedent: FlowNode, node: CallExpression): FlowNode {
978982
setFlowNodeReferenced(antecedent);
979-
return flowNodeCreated({ flags: FlowFlags.Call, antecedent, node });
980-
}
981-
982-
function createFlowArrayMutation(antecedent: FlowNode, node: CallExpression | BinaryExpression): FlowNode {
983-
setFlowNodeReferenced(antecedent);
984-
return flowNodeCreated({ flags: FlowFlags.ArrayMutation, antecedent, node });
983+
return initFlowNode({ flags: FlowFlags.Call, antecedent, node });
985984
}
986985

987986
function finishFlowLabel(flow: FlowLabel): FlowNode {
@@ -1189,93 +1188,56 @@ namespace ts {
11891188

11901189
function bindTryStatement(node: TryStatement): void {
11911190
const preFinallyLabel = createBranchLabel();
1192-
const preTryFlow = currentFlow;
1193-
const tryPriors: FlowNode[] = [];
1194-
const oldFlowNodeCreated = flowNodeCreated;
1195-
// We hook the creation of all flow nodes within the `try` scope and store them so we can add _all_ of them
1196-
// as possible antecedents of the start of the `catch` or `finally` blocks.
1197-
// Don't bother intercepting the call if there's no finally or catch block that needs the information
1198-
if (node.catchClause || node.finallyBlock) {
1199-
flowNodeCreated = node => (tryPriors.push(node), initFlowNode(node));
1200-
}
1191+
// We conservatively assume that *any* code in the try block can cause an exception, but we only need
1192+
// to track code that causes mutations (because only mutations widen the possible control flow type of
1193+
// a variable). The currentExceptionTarget is the target label for control flows that result from
1194+
// exceptions. We add all mutation flow nodes as antecedents of this label such that we can analyze them
1195+
// as possible antecedents of the start of catch or finally blocks. Furthermore, we add the current
1196+
// control flow to represent exceptions that occur before any mutations.
1197+
const saveExceptionTarget = currentExceptionTarget;
1198+
currentExceptionTarget = createBranchLabel();
1199+
addAntecedent(currentExceptionTarget, currentFlow);
12011200
bind(node.tryBlock);
1202-
flowNodeCreated = oldFlowNodeCreated;
12031201
addAntecedent(preFinallyLabel, currentFlow);
1204-
12051202
const flowAfterTry = currentFlow;
12061203
let flowAfterCatch = unreachableFlow;
1207-
12081204
if (node.catchClause) {
1209-
currentFlow = preTryFlow;
1210-
if (tryPriors.length) {
1211-
const preCatchFlow = createBranchLabel();
1212-
addAntecedent(preCatchFlow, currentFlow);
1213-
for (const p of tryPriors) {
1214-
addAntecedent(preCatchFlow, p);
1215-
}
1216-
currentFlow = finishFlowLabel(preCatchFlow);
1217-
}
1218-
1205+
// Start of catch clause is the target of exceptions from try block.
1206+
currentFlow = finishFlowLabel(currentExceptionTarget);
1207+
// The currentExceptionTarget now represents control flows from exceptions in the catch clause.
1208+
// Effectively, in a try-catch-finally, if an exception occurs in the try block, the catch block
1209+
// acts like a second try block.
1210+
currentExceptionTarget = createBranchLabel();
1211+
addAntecedent(currentExceptionTarget, currentFlow);
12191212
bind(node.catchClause);
12201213
addAntecedent(preFinallyLabel, currentFlow);
1221-
12221214
flowAfterCatch = currentFlow;
12231215
}
1216+
const exceptionTarget = finishFlowLabel(currentExceptionTarget);
1217+
currentExceptionTarget = saveExceptionTarget;
12241218
if (node.finallyBlock) {
1225-
// We add the nodes within the `try` block to the `finally`'s antecedents if there's no catch block
1226-
// (If there is a `catch` block, it will have all these antecedents instead, and the `finally` will
1227-
// have the end of the `try` block and the end of the `catch` block)
1228-
let preFinallyPrior = preTryFlow;
1229-
if (!node.catchClause) {
1230-
if (tryPriors.length) {
1231-
const preFinallyFlow = createBranchLabel();
1232-
addAntecedent(preFinallyFlow, preTryFlow);
1233-
for (const p of tryPriors) {
1234-
addAntecedent(preFinallyFlow, p);
1235-
}
1236-
preFinallyPrior = finishFlowLabel(preFinallyFlow);
1237-
}
1238-
}
1239-
1240-
// in finally flow is combined from pre-try/flow from try/flow from catch
1241-
// pre-flow is necessary to make sure that finally is reachable even if finally flows in both try and finally blocks are unreachable
1242-
1243-
// also for finally blocks we inject two extra edges into the flow graph.
1244-
// first -> edge that connects pre-try flow with the label at the beginning of the finally block, it has lock associated with it
1245-
// second -> edge that represents post-finally flow.
1246-
// these edges are used in following scenario:
1247-
// let a; (1)
1248-
// try { a = someOperation(); (2)}
1249-
// finally { (3) console.log(a) } (4)
1250-
// (5) a
1251-
1252-
// flow graph for this case looks roughly like this (arrows show ):
1253-
// (1-pre-try-flow) <--.. <-- (2-post-try-flow)
1254-
// ^ ^
1255-
// |*****(3-pre-finally-label) -----|
1256-
// ^
1257-
// |-- ... <-- (4-post-finally-label) <--- (5)
1258-
// In case when we walk the flow starting from inside the finally block we want to take edge '*****' into account
1259-
// since it ensures that finally is always reachable. However when we start outside the finally block and go through label (5)
1260-
// then edge '*****' should be discarded because label 4 is only reachable if post-finally label-4 is reachable
1261-
// Simply speaking code inside finally block is treated as reachable as pre-try-flow
1262-
// since we conservatively assume that any line in try block can throw or return in which case we'll enter finally.
1263-
// However code after finally is reachable only if control flow was not abrupted in try/catch or finally blocks - it should be composed from
1264-
// final flows of these blocks without taking pre-try flow into account.
1265-
//
1266-
// extra edges that we inject allows to control this behavior
1267-
// if when walking the flow we step on post-finally edge - we can mark matching pre-finally edge as locked so it will be skipped.
1268-
const preFinallyFlow: PreFinallyFlow = initFlowNode({ flags: FlowFlags.PreFinally, antecedent: preFinallyPrior, lock: {} });
1219+
// Possible ways control can reach the finally block:
1220+
// 1) Normal completion of try block of a try-finally or try-catch-finally
1221+
// 2) Normal completion of catch block (following exception in try block) of a try-catch-finally
1222+
// 3) Exception in try block of a try-finally
1223+
// 4) Exception in catch block of a try-catch-finally
1224+
// When analyzing a control flow graph that starts inside a finally block we want to consider all
1225+
// four possibilities above. However, when analyzing a control flow graph that starts outside (past)
1226+
// the finally block, we only want to consider the first two (if we're past a finally block then it
1227+
// must have completed normally). To make this possible, we inject two extra nodes into the control
1228+
// flow graph: An after-finally with an antecedent of the control flow at the end of the finally
1229+
// block, and a pre-finally with an antecedent that represents all exceptional control flows. The
1230+
// 'lock' property of the pre-finally references the after-finally, and the after-finally has a
1231+
// boolean 'locked' property that we set to true when analyzing a control flow that contained the
1232+
// the after-finally node. When the lock associated with a pre-finally is locked, the antecedent of
1233+
// the pre-finally (i.e. the exceptional control flows) are skipped.
1234+
const preFinallyFlow: PreFinallyFlow = initFlowNode({ flags: FlowFlags.PreFinally, antecedent: exceptionTarget, lock: {} });
12691235
addAntecedent(preFinallyLabel, preFinallyFlow);
1270-
12711236
currentFlow = finishFlowLabel(preFinallyLabel);
12721237
bind(node.finallyBlock);
1273-
// if flow after finally is unreachable - keep it
1274-
// otherwise check if flows after try and after catch are unreachable
1275-
// if yes - convert current flow to unreachable
1276-
// i.e.
1277-
// try { return "1" } finally { console.log(1); }
1278-
// console.log(2); // this line should be unreachable even if flow falls out of finally block
1238+
// If the end of the finally block is reachable, but the end of the try and catch blocks are not,
1239+
// convert the current flow to unreachable. For example, 'try { return 1; } finally { ... }' should
1240+
// result in an unreachable current control flow.
12791241
if (!(currentFlow.flags & FlowFlags.Unreachable)) {
12801242
if ((flowAfterTry.flags & FlowFlags.Unreachable) && (flowAfterCatch.flags & FlowFlags.Unreachable)) {
12811243
currentFlow = flowAfterTry === reportedUnreachableFlow || flowAfterCatch === reportedUnreachableFlow
@@ -1284,7 +1246,7 @@ namespace ts {
12841246
}
12851247
}
12861248
if (!(currentFlow.flags & FlowFlags.Unreachable)) {
1287-
const afterFinallyFlow: AfterFinallyFlow = flowNodeCreated({ flags: FlowFlags.AfterFinally, antecedent: currentFlow });
1249+
const afterFinallyFlow: AfterFinallyFlow = initFlowNode({ flags: FlowFlags.AfterFinally, antecedent: currentFlow });
12881250
preFinallyFlow.lock = afterFinallyFlow;
12891251
currentFlow = afterFinallyFlow;
12901252
}
@@ -1407,7 +1369,7 @@ namespace ts {
14071369

14081370
function bindAssignmentTargetFlow(node: Expression) {
14091371
if (isNarrowableReference(node)) {
1410-
currentFlow = createFlowAssignment(currentFlow, node);
1372+
currentFlow = createFlowMutation(FlowFlags.Assignment, currentFlow, node);
14111373
}
14121374
else if (node.kind === SyntaxKind.ArrayLiteralExpression) {
14131375
for (const e of (<ArrayLiteralExpression>node).elements) {
@@ -1490,7 +1452,7 @@ namespace ts {
14901452
if (operator === SyntaxKind.EqualsToken && node.left.kind === SyntaxKind.ElementAccessExpression) {
14911453
const elementAccess = <ElementAccessExpression>node.left;
14921454
if (isNarrowableOperand(elementAccess.expression)) {
1493-
currentFlow = createFlowArrayMutation(currentFlow, node);
1455+
currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node);
14941456
}
14951457
}
14961458
}
@@ -1528,7 +1490,7 @@ namespace ts {
15281490
}
15291491
}
15301492
else {
1531-
currentFlow = createFlowAssignment(currentFlow, node);
1493+
currentFlow = createFlowMutation(FlowFlags.Assignment, currentFlow, node);
15321494
}
15331495
}
15341496

@@ -1643,7 +1605,7 @@ namespace ts {
16431605
if (node.expression.kind === SyntaxKind.PropertyAccessExpression) {
16441606
const propertyAccess = <PropertyAccessExpression>node.expression;
16451607
if (isNarrowableOperand(propertyAccess.expression) && isPushOrUnshiftIdentifier(propertyAccess.name)) {
1646-
currentFlow = createFlowArrayMutation(currentFlow, node);
1608+
currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node);
16471609
}
16481610
}
16491611
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//// [tryCatchFinallyControlFlow.ts]
2+
// Repro from #34797
3+
4+
function f1() {
5+
let a: number | null = null;
6+
try {
7+
a = 123;
8+
return a;
9+
}
10+
catch (e) {
11+
throw e;
12+
}
13+
finally {
14+
if (a != null && a.toFixed(0) == "123") {
15+
}
16+
}
17+
}
18+
19+
function f2() {
20+
let x: 0 | 1 | 2 | 3 = 0;
21+
try {
22+
x = 1;
23+
}
24+
catch (e) {
25+
x = 2;
26+
throw e;
27+
}
28+
finally {
29+
x; // 0 | 1 | 2
30+
}
31+
x; // 1
32+
}
33+
34+
function f3() {
35+
let x: 0 | 1 | 2 | 3 = 0;
36+
try {
37+
x = 1;
38+
}
39+
catch (e) {
40+
x = 2;
41+
return;
42+
}
43+
finally {
44+
x; // 0 | 1 | 2
45+
}
46+
x; // 1
47+
}
48+
49+
function f4() {
50+
let x: 0 | 1 | 2 | 3 = 0;
51+
try {
52+
x = 1;
53+
}
54+
catch (e) {
55+
x = 2;
56+
}
57+
finally {
58+
x; // 0 | 1 | 2
59+
}
60+
x; // 1 | 2
61+
}
62+
63+
64+
//// [tryCatchFinallyControlFlow.js]
65+
"use strict";
66+
// Repro from #34797
67+
function f1() {
68+
var a = null;
69+
try {
70+
a = 123;
71+
return a;
72+
}
73+
catch (e) {
74+
throw e;
75+
}
76+
finally {
77+
if (a != null && a.toFixed(0) == "123") {
78+
}
79+
}
80+
}
81+
function f2() {
82+
var x = 0;
83+
try {
84+
x = 1;
85+
}
86+
catch (e) {
87+
x = 2;
88+
throw e;
89+
}
90+
finally {
91+
x; // 0 | 1 | 2
92+
}
93+
x; // 1
94+
}
95+
function f3() {
96+
var x = 0;
97+
try {
98+
x = 1;
99+
}
100+
catch (e) {
101+
x = 2;
102+
return;
103+
}
104+
finally {
105+
x; // 0 | 1 | 2
106+
}
107+
x; // 1
108+
}
109+
function f4() {
110+
var x = 0;
111+
try {
112+
x = 1;
113+
}
114+
catch (e) {
115+
x = 2;
116+
}
117+
finally {
118+
x; // 0 | 1 | 2
119+
}
120+
x; // 1 | 2
121+
}

0 commit comments

Comments
 (0)