From 2fe2f88047e6c1465744cf869b53810ee8d0edfc Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 3 Jul 2020 09:51:01 -0700 Subject: [PATCH] Fix control flow analysis for nested try-catch-finally statements (#39399) * Fix control flow analysis for nested try-catch-finally statements * Add tests --- src/compiler/binder.ts | 5 + .../tryCatchFinallyControlFlow.errors.txt | 70 +++++++ .../reference/tryCatchFinallyControlFlow.js | 138 +++++++++++++ .../tryCatchFinallyControlFlow.symbols | 136 +++++++++++++ .../tryCatchFinallyControlFlow.types | 188 ++++++++++++++++++ .../compiler/tryCatchFinallyControlFlow.ts | 70 +++++++ 6 files changed, 607 insertions(+) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index ff617602d43..b9126ca464c 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1241,6 +1241,11 @@ namespace ts { if (currentReturnTarget && returnLabel.antecedents) { addAntecedent(currentReturnTarget, createReduceLabel(finallyLabel, returnLabel.antecedents, currentFlow)); } + // If we have an outer exception target (i.e. a containing try-finally or try-catch-finally), add a + // control flow that goes back through the finally blok and back through each possible exception source. + if (currentExceptionTarget && exceptionLabel.antecedents) { + addAntecedent(currentExceptionTarget, createReduceLabel(finallyLabel, exceptionLabel.antecedents, currentFlow)); + } // 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. diff --git a/tests/baselines/reference/tryCatchFinallyControlFlow.errors.txt b/tests/baselines/reference/tryCatchFinallyControlFlow.errors.txt index b9be0305827..a6972d05d8e 100644 --- a/tests/baselines/reference/tryCatchFinallyControlFlow.errors.txt +++ b/tests/baselines/reference/tryCatchFinallyControlFlow.errors.txt @@ -275,4 +275,74 @@ tests/cases/compiler/tryCatchFinallyControlFlow.ts(255,9): error TS7027: Unreach })(); x; // Reachable } + + // Repro from #39043 + + type State = { tag: "one" } | { tag: "two" } | { tag: "three" }; + + function notallowed(arg: number) { + let state: State = { tag: "one" }; + try { + state = { tag: "two" }; + try { + state = { tag: "three" }; + } + finally { } + } + catch (err) { + state.tag; + if (state.tag !== "one" && state.tag !== "two") { + console.log(state.tag); + } + } + } + + function f20() { + let x: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0; + try { + x = 1; + try { + x = 2; + try { + x = 3; + } + finally { + if (!!true) x = 4; + } + x; // 3 | 4 + } + finally { + if (!!true) x = 5; + } + x; // 3 | 4 | 5 + } + finally { + if (!!true) x = 6; + } + x; // 3 | 4 | 5 | 6 + } + + function f21() { + let x: 0 | 1 | 2 | 3 | 4 | 5 = 0; + try { + x = 1; + try { + x = 2; + try { + x = 3; + } + finally { + if (!!true) x = 4; + } + x; // 3 | 4 + } + finally { + if (!!true) x = 5; + } + x; // 3 | 4 | 5 + } + catch (e) { + x; // 0 | 1 | 2 | 3 | 4 | 5 + } + } \ No newline at end of file diff --git a/tests/baselines/reference/tryCatchFinallyControlFlow.js b/tests/baselines/reference/tryCatchFinallyControlFlow.js index 2ccecd0cf20..32eed6689ba 100644 --- a/tests/baselines/reference/tryCatchFinallyControlFlow.js +++ b/tests/baselines/reference/tryCatchFinallyControlFlow.js @@ -257,6 +257,76 @@ function t1() { })(); x; // Reachable } + +// Repro from #39043 + +type State = { tag: "one" } | { tag: "two" } | { tag: "three" }; + +function notallowed(arg: number) { + let state: State = { tag: "one" }; + try { + state = { tag: "two" }; + try { + state = { tag: "three" }; + } + finally { } + } + catch (err) { + state.tag; + if (state.tag !== "one" && state.tag !== "two") { + console.log(state.tag); + } + } +} + +function f20() { + let x: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0; + try { + x = 1; + try { + x = 2; + try { + x = 3; + } + finally { + if (!!true) x = 4; + } + x; // 3 | 4 + } + finally { + if (!!true) x = 5; + } + x; // 3 | 4 | 5 + } + finally { + if (!!true) x = 6; + } + x; // 3 | 4 | 5 | 6 +} + +function f21() { + let x: 0 | 1 | 2 | 3 | 4 | 5 = 0; + try { + x = 1; + try { + x = 2; + try { + x = 3; + } + finally { + if (!!true) x = 4; + } + x; // 3 | 4 + } + finally { + if (!!true) x = 5; + } + x; // 3 | 4 | 5 + } + catch (e) { + x; // 0 | 1 | 2 | 3 | 4 | 5 + } +} //// [tryCatchFinallyControlFlow.js] @@ -503,3 +573,71 @@ function t1() { })(); x; // Reachable } +function notallowed(arg) { + var state = { tag: "one" }; + try { + state = { tag: "two" }; + try { + state = { tag: "three" }; + } + finally { } + } + catch (err) { + state.tag; + if (state.tag !== "one" && state.tag !== "two") { + console.log(state.tag); + } + } +} +function f20() { + var x = 0; + try { + x = 1; + try { + x = 2; + try { + x = 3; + } + finally { + if (!!true) + x = 4; + } + x; // 3 | 4 + } + finally { + if (!!true) + x = 5; + } + x; // 3 | 4 | 5 + } + finally { + if (!!true) + x = 6; + } + x; // 3 | 4 | 5 | 6 +} +function f21() { + var x = 0; + try { + x = 1; + try { + x = 2; + try { + x = 3; + } + finally { + if (!!true) + x = 4; + } + x; // 3 | 4 + } + finally { + if (!!true) + x = 5; + } + x; // 3 | 4 | 5 + } + catch (e) { + x; // 0 | 1 | 2 | 3 | 4 | 5 + } +} diff --git a/tests/baselines/reference/tryCatchFinallyControlFlow.symbols b/tests/baselines/reference/tryCatchFinallyControlFlow.symbols index 30e6ea3da35..94a4c006ec4 100644 --- a/tests/baselines/reference/tryCatchFinallyControlFlow.symbols +++ b/tests/baselines/reference/tryCatchFinallyControlFlow.symbols @@ -445,3 +445,139 @@ function t1() { >x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 247, 9)) } +// Repro from #39043 + +type State = { tag: "one" } | { tag: "two" } | { tag: "three" }; +>State : Symbol(State, Decl(tryCatchFinallyControlFlow.ts, 257, 1)) +>tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 14)) +>tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 31)) +>tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 48)) + +function notallowed(arg: number) { +>notallowed : Symbol(notallowed, Decl(tryCatchFinallyControlFlow.ts, 261, 64)) +>arg : Symbol(arg, Decl(tryCatchFinallyControlFlow.ts, 263, 20)) + + let state: State = { tag: "one" }; +>state : Symbol(state, Decl(tryCatchFinallyControlFlow.ts, 264, 7)) +>State : Symbol(State, Decl(tryCatchFinallyControlFlow.ts, 257, 1)) +>tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 264, 24)) + + try { + state = { tag: "two" }; +>state : Symbol(state, Decl(tryCatchFinallyControlFlow.ts, 264, 7)) +>tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 266, 17)) + + try { + state = { tag: "three" }; +>state : Symbol(state, Decl(tryCatchFinallyControlFlow.ts, 264, 7)) +>tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 268, 21)) + } + finally { } + } + catch (err) { +>err : Symbol(err, Decl(tryCatchFinallyControlFlow.ts, 272, 11)) + + state.tag; +>state.tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 14), Decl(tryCatchFinallyControlFlow.ts, 261, 31), Decl(tryCatchFinallyControlFlow.ts, 261, 48)) +>state : Symbol(state, Decl(tryCatchFinallyControlFlow.ts, 264, 7)) +>tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 14), Decl(tryCatchFinallyControlFlow.ts, 261, 31), Decl(tryCatchFinallyControlFlow.ts, 261, 48)) + + if (state.tag !== "one" && state.tag !== "two") { +>state.tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 14), Decl(tryCatchFinallyControlFlow.ts, 261, 31), Decl(tryCatchFinallyControlFlow.ts, 261, 48)) +>state : Symbol(state, Decl(tryCatchFinallyControlFlow.ts, 264, 7)) +>tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 14), Decl(tryCatchFinallyControlFlow.ts, 261, 31), Decl(tryCatchFinallyControlFlow.ts, 261, 48)) +>state.tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 31), Decl(tryCatchFinallyControlFlow.ts, 261, 48)) +>state : Symbol(state, Decl(tryCatchFinallyControlFlow.ts, 264, 7)) +>tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 31), Decl(tryCatchFinallyControlFlow.ts, 261, 48)) + + console.log(state.tag); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>state.tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 48)) +>state : Symbol(state, Decl(tryCatchFinallyControlFlow.ts, 264, 7)) +>tag : Symbol(tag, Decl(tryCatchFinallyControlFlow.ts, 261, 48)) + } + } +} + +function f20() { +>f20 : Symbol(f20, Decl(tryCatchFinallyControlFlow.ts, 278, 1)) + + let x: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 281, 7)) + + try { + x = 1; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 281, 7)) + + try { + x = 2; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 281, 7)) + + try { + x = 3; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 281, 7)) + } + finally { + if (!!true) x = 4; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 281, 7)) + } + x; // 3 | 4 +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 281, 7)) + } + finally { + if (!!true) x = 5; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 281, 7)) + } + x; // 3 | 4 | 5 +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 281, 7)) + } + finally { + if (!!true) x = 6; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 281, 7)) + } + x; // 3 | 4 | 5 | 6 +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 281, 7)) +} + +function f21() { +>f21 : Symbol(f21, Decl(tryCatchFinallyControlFlow.ts, 303, 1)) + + let x: 0 | 1 | 2 | 3 | 4 | 5 = 0; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 306, 7)) + + try { + x = 1; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 306, 7)) + + try { + x = 2; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 306, 7)) + + try { + x = 3; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 306, 7)) + } + finally { + if (!!true) x = 4; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 306, 7)) + } + x; // 3 | 4 +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 306, 7)) + } + finally { + if (!!true) x = 5; +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 306, 7)) + } + x; // 3 | 4 | 5 +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 306, 7)) + } + catch (e) { +>e : Symbol(e, Decl(tryCatchFinallyControlFlow.ts, 324, 11)) + + x; // 0 | 1 | 2 | 3 | 4 | 5 +>x : Symbol(x, Decl(tryCatchFinallyControlFlow.ts, 306, 7)) + } +} + diff --git a/tests/baselines/reference/tryCatchFinallyControlFlow.types b/tests/baselines/reference/tryCatchFinallyControlFlow.types index 70c11b87dca..1674907b3a2 100644 --- a/tests/baselines/reference/tryCatchFinallyControlFlow.types +++ b/tests/baselines/reference/tryCatchFinallyControlFlow.types @@ -589,3 +589,191 @@ function t1() { >x : "x" | null } +// Repro from #39043 + +type State = { tag: "one" } | { tag: "two" } | { tag: "three" }; +>State : State +>tag : "one" +>tag : "two" +>tag : "three" + +function notallowed(arg: number) { +>notallowed : (arg: number) => void +>arg : number + + let state: State = { tag: "one" }; +>state : State +>{ tag: "one" } : { tag: "one"; } +>tag : "one" +>"one" : "one" + + try { + state = { tag: "two" }; +>state = { tag: "two" } : { tag: "two"; } +>state : State +>{ tag: "two" } : { tag: "two"; } +>tag : "two" +>"two" : "two" + + try { + state = { tag: "three" }; +>state = { tag: "three" } : { tag: "three"; } +>state : State +>{ tag: "three" } : { tag: "three"; } +>tag : "three" +>"three" : "three" + } + finally { } + } + catch (err) { +>err : any + + state.tag; +>state.tag : "one" | "two" | "three" +>state : State +>tag : "one" | "two" | "three" + + if (state.tag !== "one" && state.tag !== "two") { +>state.tag !== "one" && state.tag !== "two" : boolean +>state.tag !== "one" : boolean +>state.tag : "one" | "two" | "three" +>state : State +>tag : "one" | "two" | "three" +>"one" : "one" +>state.tag !== "two" : boolean +>state.tag : "two" | "three" +>state : { tag: "two"; } | { tag: "three"; } +>tag : "two" | "three" +>"two" : "two" + + console.log(state.tag); +>console.log(state.tag) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>state.tag : "three" +>state : { tag: "three"; } +>tag : "three" + } + } +} + +function f20() { +>f20 : () => void + + let x: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0; +>x : 0 | 1 | 2 | 3 | 4 | 5 | 6 +>0 : 0 + + try { + x = 1; +>x = 1 : 1 +>x : 0 | 1 | 2 | 3 | 4 | 5 | 6 +>1 : 1 + + try { + x = 2; +>x = 2 : 2 +>x : 0 | 1 | 2 | 3 | 4 | 5 | 6 +>2 : 2 + + try { + x = 3; +>x = 3 : 3 +>x : 0 | 1 | 2 | 3 | 4 | 5 | 6 +>3 : 3 + } + finally { + if (!!true) x = 4; +>!!true : true +>!true : false +>true : true +>x = 4 : 4 +>x : 0 | 1 | 2 | 3 | 4 | 5 | 6 +>4 : 4 + } + x; // 3 | 4 +>x : 3 | 4 + } + finally { + if (!!true) x = 5; +>!!true : true +>!true : false +>true : true +>x = 5 : 5 +>x : 0 | 1 | 2 | 3 | 4 | 5 | 6 +>5 : 5 + } + x; // 3 | 4 | 5 +>x : 3 | 4 | 5 + } + finally { + if (!!true) x = 6; +>!!true : true +>!true : false +>true : true +>x = 6 : 6 +>x : 0 | 1 | 2 | 3 | 4 | 5 | 6 +>6 : 6 + } + x; // 3 | 4 | 5 | 6 +>x : 3 | 4 | 5 | 6 +} + +function f21() { +>f21 : () => void + + let x: 0 | 1 | 2 | 3 | 4 | 5 = 0; +>x : 0 | 1 | 2 | 3 | 4 | 5 +>0 : 0 + + try { + x = 1; +>x = 1 : 1 +>x : 0 | 1 | 2 | 3 | 4 | 5 +>1 : 1 + + try { + x = 2; +>x = 2 : 2 +>x : 0 | 1 | 2 | 3 | 4 | 5 +>2 : 2 + + try { + x = 3; +>x = 3 : 3 +>x : 0 | 1 | 2 | 3 | 4 | 5 +>3 : 3 + } + finally { + if (!!true) x = 4; +>!!true : true +>!true : false +>true : true +>x = 4 : 4 +>x : 0 | 1 | 2 | 3 | 4 | 5 +>4 : 4 + } + x; // 3 | 4 +>x : 3 | 4 + } + finally { + if (!!true) x = 5; +>!!true : true +>!true : false +>true : true +>x = 5 : 5 +>x : 0 | 1 | 2 | 3 | 4 | 5 +>5 : 5 + } + x; // 3 | 4 | 5 +>x : 3 | 4 | 5 + } + catch (e) { +>e : any + + x; // 0 | 1 | 2 | 3 | 4 | 5 +>x : 0 | 1 | 2 | 3 | 4 | 5 + } +} + diff --git a/tests/cases/compiler/tryCatchFinallyControlFlow.ts b/tests/cases/compiler/tryCatchFinallyControlFlow.ts index 83909b5ddb0..c2f19bbe5bb 100644 --- a/tests/cases/compiler/tryCatchFinallyControlFlow.ts +++ b/tests/cases/compiler/tryCatchFinallyControlFlow.ts @@ -259,3 +259,73 @@ function t1() { })(); x; // Reachable } + +// Repro from #39043 + +type State = { tag: "one" } | { tag: "two" } | { tag: "three" }; + +function notallowed(arg: number) { + let state: State = { tag: "one" }; + try { + state = { tag: "two" }; + try { + state = { tag: "three" }; + } + finally { } + } + catch (err) { + state.tag; + if (state.tag !== "one" && state.tag !== "two") { + console.log(state.tag); + } + } +} + +function f20() { + let x: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0; + try { + x = 1; + try { + x = 2; + try { + x = 3; + } + finally { + if (!!true) x = 4; + } + x; // 3 | 4 + } + finally { + if (!!true) x = 5; + } + x; // 3 | 4 | 5 + } + finally { + if (!!true) x = 6; + } + x; // 3 | 4 | 5 | 6 +} + +function f21() { + let x: 0 | 1 | 2 | 3 | 4 | 5 = 0; + try { + x = 1; + try { + x = 2; + try { + x = 3; + } + finally { + if (!!true) x = 4; + } + x; // 3 | 4 + } + finally { + if (!!true) x = 5; + } + x; // 3 | 4 | 5 + } + catch (e) { + x; // 0 | 1 | 2 | 3 | 4 | 5 + } +}