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
This commit is contained in:
Anders Hejlsberg 2019-11-05 12:06:25 -08:00 committed by GitHub
parent 95be956320
commit 1c42c1aaa8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 483 additions and 94 deletions

View file

@ -167,8 +167,6 @@ namespace ts {
return node;
}
let flowNodeCreated: <T extends FlowNode>(node: T) => T = initFlowNode;
const binder = createBinder();
export function bindSourceFile(file: SourceFile, options: CompilerOptions) {
@ -199,6 +197,7 @@ namespace ts {
let currentReturnTarget: FlowLabel | undefined;
let currentTrueTarget: FlowLabel | undefined;
let currentFalseTarget: FlowLabel | undefined;
let currentExceptionTarget: FlowLabel | undefined;
let preSwitchCaseFlow: FlowNode | undefined;
let activeLabels: ActiveLabel[] | undefined;
let hasExplicitReturn: boolean;
@ -271,6 +270,7 @@ namespace ts {
currentReturnTarget = undefined;
currentTrueTarget = undefined;
currentFalseTarget = undefined;
currentExceptionTarget = undefined;
activeLabels = undefined!;
hasExplicitReturn = false;
emitFlags = NodeFlags.None;
@ -624,11 +624,11 @@ namespace ts {
blockScopeContainer.locals = undefined;
}
if (containerFlags & ContainerFlags.IsControlFlowContainer) {
const saveFlowNodeCreated = flowNodeCreated;
const saveCurrentFlow = currentFlow;
const saveBreakTarget = currentBreakTarget;
const saveContinueTarget = currentContinueTarget;
const saveReturnTarget = currentReturnTarget;
const saveExceptionTarget = currentExceptionTarget;
const saveActiveLabels = activeLabels;
const saveHasExplicitReturn = hasExplicitReturn;
const isIIFE = containerFlags & ContainerFlags.IsFunctionExpression && !hasModifier(node, ModifierFlags.Async) &&
@ -644,11 +644,11 @@ namespace ts {
// We create a return control flow graph for IIFEs and constructors. For constructors
// we use the return control flow graph in strict property initialization checks.
currentReturnTarget = isIIFE || node.kind === SyntaxKind.Constructor ? createBranchLabel() : undefined;
currentExceptionTarget = undefined;
currentBreakTarget = undefined;
currentContinueTarget = undefined;
activeLabels = undefined;
hasExplicitReturn = false;
flowNodeCreated = initFlowNode;
bindChildren(node);
// Reset all reachability check related flags on node (for incremental scenarios)
node.flags &= ~NodeFlags.ReachabilityAndEmitFlags;
@ -674,9 +674,9 @@ namespace ts {
currentBreakTarget = saveBreakTarget;
currentContinueTarget = saveContinueTarget;
currentReturnTarget = saveReturnTarget;
currentExceptionTarget = saveExceptionTarget;
activeLabels = saveActiveLabels;
hasExplicitReturn = saveHasExplicitReturn;
flowNodeCreated = saveFlowNodeCreated;
}
else if (containerFlags & ContainerFlags.IsInterface) {
seenThisKeyword = false;
@ -961,27 +961,26 @@ namespace ts {
return antecedent;
}
setFlowNodeReferenced(antecedent);
return flowNodeCreated({ flags, antecedent, node: expression });
return initFlowNode({ flags, antecedent, node: expression });
}
function createFlowSwitchClause(antecedent: FlowNode, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): FlowNode {
setFlowNodeReferenced(antecedent);
return flowNodeCreated({ flags: FlowFlags.SwitchClause, antecedent, switchStatement, clauseStart, clauseEnd });
return initFlowNode({ flags: FlowFlags.SwitchClause, antecedent, switchStatement, clauseStart, clauseEnd });
}
function createFlowAssignment(antecedent: FlowNode, node: Expression | VariableDeclaration | BindingElement): FlowNode {
function createFlowMutation(flags: FlowFlags, antecedent: FlowNode, node: Node): FlowNode {
setFlowNodeReferenced(antecedent);
return flowNodeCreated({ flags: FlowFlags.Assignment, antecedent, node });
const result = initFlowNode({ flags, antecedent, node });
if (currentExceptionTarget) {
addAntecedent(currentExceptionTarget, result);
}
return result;
}
function createFlowCall(antecedent: FlowNode, node: CallExpression): FlowNode {
setFlowNodeReferenced(antecedent);
return flowNodeCreated({ flags: FlowFlags.Call, antecedent, node });
}
function createFlowArrayMutation(antecedent: FlowNode, node: CallExpression | BinaryExpression): FlowNode {
setFlowNodeReferenced(antecedent);
return flowNodeCreated({ flags: FlowFlags.ArrayMutation, antecedent, node });
return initFlowNode({ flags: FlowFlags.Call, antecedent, node });
}
function finishFlowLabel(flow: FlowLabel): FlowNode {
@ -1189,93 +1188,56 @@ namespace ts {
function bindTryStatement(node: TryStatement): void {
const preFinallyLabel = createBranchLabel();
const preTryFlow = currentFlow;
const tryPriors: FlowNode[] = [];
const oldFlowNodeCreated = flowNodeCreated;
// We hook the creation of all flow nodes within the `try` scope and store them so we can add _all_ of them
// as possible antecedents of the start of the `catch` or `finally` blocks.
// Don't bother intercepting the call if there's no finally or catch block that needs the information
if (node.catchClause || node.finallyBlock) {
flowNodeCreated = node => (tryPriors.push(node), initFlowNode(node));
}
// We conservatively assume that *any* code in the try block can cause an exception, but we only need
// to track code that causes mutations (because only mutations widen the possible control flow type of
// a variable). The currentExceptionTarget is the target label for control flows that result from
// exceptions. We add all mutation flow nodes as antecedents of this label such that we can analyze them
// as possible antecedents of the start of catch or finally blocks. Furthermore, we add the current
// control flow to represent exceptions that occur before any mutations.
const saveExceptionTarget = currentExceptionTarget;
currentExceptionTarget = createBranchLabel();
addAntecedent(currentExceptionTarget, currentFlow);
bind(node.tryBlock);
flowNodeCreated = oldFlowNodeCreated;
addAntecedent(preFinallyLabel, currentFlow);
const flowAfterTry = currentFlow;
let flowAfterCatch = unreachableFlow;
if (node.catchClause) {
currentFlow = preTryFlow;
if (tryPriors.length) {
const preCatchFlow = createBranchLabel();
addAntecedent(preCatchFlow, currentFlow);
for (const p of tryPriors) {
addAntecedent(preCatchFlow, p);
}
currentFlow = finishFlowLabel(preCatchFlow);
}
// Start of catch clause is the target of exceptions from try block.
currentFlow = finishFlowLabel(currentExceptionTarget);
// The currentExceptionTarget now represents control flows from exceptions in the catch clause.
// Effectively, in a try-catch-finally, if an exception occurs in the try block, the catch block
// acts like a second try block.
currentExceptionTarget = createBranchLabel();
addAntecedent(currentExceptionTarget, currentFlow);
bind(node.catchClause);
addAntecedent(preFinallyLabel, currentFlow);
flowAfterCatch = currentFlow;
}
const exceptionTarget = finishFlowLabel(currentExceptionTarget);
currentExceptionTarget = saveExceptionTarget;
if (node.finallyBlock) {
// We add the nodes within the `try` block to the `finally`'s antecedents if there's no catch block
// (If there is a `catch` block, it will have all these antecedents instead, and the `finally` will
// have the end of the `try` block and the end of the `catch` block)
let preFinallyPrior = preTryFlow;
if (!node.catchClause) {
if (tryPriors.length) {
const preFinallyFlow = createBranchLabel();
addAntecedent(preFinallyFlow, preTryFlow);
for (const p of tryPriors) {
addAntecedent(preFinallyFlow, p);
}
preFinallyPrior = finishFlowLabel(preFinallyFlow);
}
}
// in finally flow is combined from pre-try/flow from try/flow from catch
// pre-flow is necessary to make sure that finally is reachable even if finally flows in both try and finally blocks are unreachable
// also for finally blocks we inject two extra edges into the flow graph.
// first -> edge that connects pre-try flow with the label at the beginning of the finally block, it has lock associated with it
// second -> edge that represents post-finally flow.
// these edges are used in following scenario:
// let a; (1)
// try { a = someOperation(); (2)}
// finally { (3) console.log(a) } (4)
// (5) a
// flow graph for this case looks roughly like this (arrows show ):
// (1-pre-try-flow) <--.. <-- (2-post-try-flow)
// ^ ^
// |*****(3-pre-finally-label) -----|
// ^
// |-- ... <-- (4-post-finally-label) <--- (5)
// In case when we walk the flow starting from inside the finally block we want to take edge '*****' into account
// since it ensures that finally is always reachable. However when we start outside the finally block and go through label (5)
// then edge '*****' should be discarded because label 4 is only reachable if post-finally label-4 is reachable
// Simply speaking code inside finally block is treated as reachable as pre-try-flow
// since we conservatively assume that any line in try block can throw or return in which case we'll enter finally.
// However code after finally is reachable only if control flow was not abrupted in try/catch or finally blocks - it should be composed from
// final flows of these blocks without taking pre-try flow into account.
//
// extra edges that we inject allows to control this behavior
// 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.
const preFinallyFlow: PreFinallyFlow = initFlowNode({ flags: FlowFlags.PreFinally, antecedent: preFinallyPrior, lock: {} });
// Possible ways control can reach the finally block:
// 1) Normal completion of try block of a try-finally or try-catch-finally
// 2) Normal completion of catch block (following exception in try block) of a try-catch-finally
// 3) Exception in try block of a try-finally
// 4) Exception in catch block of a try-catch-finally
// When analyzing a control flow graph that starts inside a finally block we want to consider all
// four possibilities above. However, when analyzing a control flow graph that starts outside (past)
// the finally block, we only want to consider the first two (if we're past a finally block then it
// must have completed normally). To make this possible, we inject two extra nodes into the control
// flow graph: An after-finally with an antecedent of the control flow at the end of the finally
// block, and a pre-finally with an antecedent that represents all exceptional control flows. The
// 'lock' property of the pre-finally references the after-finally, and the after-finally has a
// boolean 'locked' property that we set to true when analyzing a control flow that contained the
// the after-finally node. When the lock associated with a pre-finally is locked, the antecedent of
// the pre-finally (i.e. the exceptional control flows) are skipped.
const preFinallyFlow: PreFinallyFlow = initFlowNode({ flags: FlowFlags.PreFinally, antecedent: exceptionTarget, lock: {} });
addAntecedent(preFinallyLabel, preFinallyFlow);
currentFlow = finishFlowLabel(preFinallyLabel);
bind(node.finallyBlock);
// if flow after finally is unreachable - keep it
// otherwise check if flows after try and after catch are unreachable
// if yes - convert current flow to unreachable
// i.e.
// try { return "1" } finally { console.log(1); }
// console.log(2); // this line should be unreachable even if flow falls out of finally block
// If the end of the finally block is reachable, but the end of the try and catch blocks are not,
// convert the current flow to unreachable. For example, 'try { return 1; } finally { ... }' should
// result in an unreachable current control flow.
if (!(currentFlow.flags & FlowFlags.Unreachable)) {
if ((flowAfterTry.flags & FlowFlags.Unreachable) && (flowAfterCatch.flags & FlowFlags.Unreachable)) {
currentFlow = flowAfterTry === reportedUnreachableFlow || flowAfterCatch === reportedUnreachableFlow
@ -1284,7 +1246,7 @@ namespace ts {
}
}
if (!(currentFlow.flags & FlowFlags.Unreachable)) {
const afterFinallyFlow: AfterFinallyFlow = flowNodeCreated({ flags: FlowFlags.AfterFinally, antecedent: currentFlow });
const afterFinallyFlow: AfterFinallyFlow = initFlowNode({ flags: FlowFlags.AfterFinally, antecedent: currentFlow });
preFinallyFlow.lock = afterFinallyFlow;
currentFlow = afterFinallyFlow;
}
@ -1407,7 +1369,7 @@ namespace ts {
function bindAssignmentTargetFlow(node: Expression) {
if (isNarrowableReference(node)) {
currentFlow = createFlowAssignment(currentFlow, node);
currentFlow = createFlowMutation(FlowFlags.Assignment, currentFlow, node);
}
else if (node.kind === SyntaxKind.ArrayLiteralExpression) {
for (const e of (<ArrayLiteralExpression>node).elements) {
@ -1490,7 +1452,7 @@ namespace ts {
if (operator === SyntaxKind.EqualsToken && node.left.kind === SyntaxKind.ElementAccessExpression) {
const elementAccess = <ElementAccessExpression>node.left;
if (isNarrowableOperand(elementAccess.expression)) {
currentFlow = createFlowArrayMutation(currentFlow, node);
currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node);
}
}
}
@ -1528,7 +1490,7 @@ namespace ts {
}
}
else {
currentFlow = createFlowAssignment(currentFlow, node);
currentFlow = createFlowMutation(FlowFlags.Assignment, currentFlow, node);
}
}
@ -1643,7 +1605,7 @@ namespace ts {
if (node.expression.kind === SyntaxKind.PropertyAccessExpression) {
const propertyAccess = <PropertyAccessExpression>node.expression;
if (isNarrowableOperand(propertyAccess.expression) && isPushOrUnshiftIdentifier(propertyAccess.name)) {
currentFlow = createFlowArrayMutation(currentFlow, node);
currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node);
}
}
}

View file

@ -0,0 +1,121 @@
//// [tryCatchFinallyControlFlow.ts]
// Repro from #34797
function f1() {
let a: number | null = null;
try {
a = 123;
return a;
}
catch (e) {
throw e;
}
finally {
if (a != null && a.toFixed(0) == "123") {
}
}
}
function f2() {
let x: 0 | 1 | 2 | 3 = 0;
try {
x = 1;
}
catch (e) {
x = 2;
throw e;
}
finally {
x; // 0 | 1 | 2
}
x; // 1
}
function f3() {
let x: 0 | 1 | 2 | 3 = 0;
try {
x = 1;
}
catch (e) {
x = 2;
return;
}
finally {
x; // 0 | 1 | 2
}
x; // 1
}
function f4() {
let x: 0 | 1 | 2 | 3 = 0;
try {
x = 1;
}
catch (e) {
x = 2;
}
finally {
x; // 0 | 1 | 2
}
x; // 1 | 2
}
//// [tryCatchFinallyControlFlow.js]
"use strict";
// Repro from #34797
function f1() {
var a = null;
try {
a = 123;
return a;
}
catch (e) {
throw e;
}
finally {
if (a != null && a.toFixed(0) == "123") {
}
}
}
function f2() {
var x = 0;
try {
x = 1;
}
catch (e) {
x = 2;
throw e;
}
finally {
x; // 0 | 1 | 2
}
x; // 1
}
function f3() {
var x = 0;
try {
x = 1;
}
catch (e) {
x = 2;
return;
}
finally {
x; // 0 | 1 | 2
}
x; // 1
}
function f4() {
var x = 0;
try {
x = 1;
}
catch (e) {
x = 2;
}
finally {
x; // 0 | 1 | 2
}
x; // 1 | 2
}

View file

@ -0,0 +1,109 @@
=== tests/cases/compiler/tryCatchFinallyControlFlow.ts ===
// Repro from #34797
function f1() {
>f1 : Symbol(f1, Decl(tryCatchFinallyControlFlow.ts, 0, 0))
let a: number | null = null;
>a : Symbol(a, Decl(tryCatchFinallyControlFlow.ts, 3, 7))
try {
a = 123;
>a : Symbol(a, Decl(tryCatchFinallyControlFlow.ts, 3, 7))
return a;
>a : Symbol(a, Decl(tryCatchFinallyControlFlow.ts, 3, 7))
}
catch (e) {
>e : Symbol(e, Decl(tryCatchFinallyControlFlow.ts, 8, 11))
throw e;
>e : Symbol(e, Decl(tryCatchFinallyControlFlow.ts, 8, 11))
}
finally {
if (a != null && a.toFixed(0) == "123") {
>a : Symbol(a, Decl(tryCatchFinallyControlFlow.ts, 3, 7))
>a.toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --))
>a : Symbol(a, Decl(tryCatchFinallyControlFlow.ts, 3, 7))
>toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --))
}
}
}
function f2() {
>f2 : Symbol(f2, Decl(tryCatchFinallyControlFlow.ts, 15, 1))
let x: 0 | 1 | 2 | 3 = 0;
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 18, 7))
try {
x = 1;
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 18, 7))
}
catch (e) {
>e : Symbol(e, Decl(tryCatchFinallyControlFlow.ts, 22, 11))
x = 2;
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 18, 7))
throw e;
>e : Symbol(e, Decl(tryCatchFinallyControlFlow.ts, 22, 11))
}
finally {
x; // 0 | 1 | 2
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 18, 7))
}
x; // 1
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 18, 7))
}
function f3() {
>f3 : Symbol(f3, Decl(tryCatchFinallyControlFlow.ts, 30, 1))
let x: 0 | 1 | 2 | 3 = 0;
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 33, 7))
try {
x = 1;
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 33, 7))
}
catch (e) {
>e : Symbol(e, Decl(tryCatchFinallyControlFlow.ts, 37, 11))
x = 2;
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 33, 7))
return;
}
finally {
x; // 0 | 1 | 2
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 33, 7))
}
x; // 1
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 33, 7))
}
function f4() {
>f4 : Symbol(f4, Decl(tryCatchFinallyControlFlow.ts, 45, 1))
let x: 0 | 1 | 2 | 3 = 0;
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 48, 7))
try {
x = 1;
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 48, 7))
}
catch (e) {
>e : Symbol(e, Decl(tryCatchFinallyControlFlow.ts, 52, 11))
x = 2;
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 48, 7))
}
finally {
x; // 0 | 1 | 2
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 48, 7))
}
x; // 1 | 2
>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 48, 7))
}

View file

@ -0,0 +1,135 @@
=== tests/cases/compiler/tryCatchFinallyControlFlow.ts ===
// Repro from #34797
function f1() {
>f1 : () => number
let a: number | null = null;
>a : number | null
>null : null
>null : null
try {
a = 123;
>a = 123 : 123
>a : number | null
>123 : 123
return a;
>a : number
}
catch (e) {
>e : any
throw e;
>e : any
}
finally {
if (a != null && a.toFixed(0) == "123") {
>a != null && a.toFixed(0) == "123" : boolean
>a != null : boolean
>a : number | null
>null : null
>a.toFixed(0) == "123" : boolean
>a.toFixed(0) : string
>a.toFixed : (fractionDigits?: number | undefined) => string
>a : number
>toFixed : (fractionDigits?: number | undefined) => string
>0 : 0
>"123" : "123"
}
}
}
function f2() {
>f2 : () => void
let x: 0 | 1 | 2 | 3 = 0;
>x : 0 | 1 | 2 | 3
>0 : 0
try {
x = 1;
>x = 1 : 1
>x : 0 | 1 | 2 | 3
>1 : 1
}
catch (e) {
>e : any
x = 2;
>x = 2 : 2
>x : 0 | 1 | 2 | 3
>2 : 2
throw e;
>e : any
}
finally {
x; // 0 | 1 | 2
>x : 0 | 1 | 2
}
x; // 1
>x : 1
}
function f3() {
>f3 : () => void
let x: 0 | 1 | 2 | 3 = 0;
>x : 0 | 1 | 2 | 3
>0 : 0
try {
x = 1;
>x = 1 : 1
>x : 0 | 1 | 2 | 3
>1 : 1
}
catch (e) {
>e : any
x = 2;
>x = 2 : 2
>x : 0 | 1 | 2 | 3
>2 : 2
return;
}
finally {
x; // 0 | 1 | 2
>x : 0 | 1 | 2
}
x; // 1
>x : 1
}
function f4() {
>f4 : () => void
let x: 0 | 1 | 2 | 3 = 0;
>x : 0 | 1 | 2 | 3
>0 : 0
try {
x = 1;
>x = 1 : 1
>x : 0 | 1 | 2 | 3
>1 : 1
}
catch (e) {
>e : any
x = 2;
>x = 2 : 2
>x : 0 | 1 | 2 | 3
>2 : 2
}
finally {
x; // 0 | 1 | 2
>x : 0 | 1 | 2
}
x; // 1 | 2
>x : 1 | 2
}

View file

@ -0,0 +1,62 @@
// @strict: true
// Repro from #34797
function f1() {
let a: number | null = null;
try {
a = 123;
return a;
}
catch (e) {
throw e;
}
finally {
if (a != null && a.toFixed(0) == "123") {
}
}
}
function f2() {
let x: 0 | 1 | 2 | 3 = 0;
try {
x = 1;
}
catch (e) {
x = 2;
throw e;
}
finally {
x; // 0 | 1 | 2
}
x; // 1
}
function f3() {
let x: 0 | 1 | 2 | 3 = 0;
try {
x = 1;
}
catch (e) {
x = 2;
return;
}
finally {
x; // 0 | 1 | 2
}
x; // 1
}
function f4() {
let x: 0 | 1 | 2 | 3 = 0;
try {
x = 1;
}
catch (e) {
x = 2;
}
finally {
x; // 0 | 1 | 2
}
x; // 1 | 2
}