Skip to content
Closed
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
11 changes: 10 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2364,6 +2364,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var sharedFlowTypes: FlowType[] = [];
var flowNodeReachable: (boolean | undefined)[] = [];
var flowNodePostSuper: (boolean | undefined)[] = [];
var reduceLabelDepth = 0;
var potentialThisCollisions: Node[] = [];
var potentialNewTargetCollisions: Node[] = [];
var potentialWeakMapSetCollisions: Node[] = [];
Expand Down Expand Up @@ -28867,7 +28868,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
const flags = flow.flags;
if (flags & FlowFlags.Shared) {
if (!noCacheCheck) {
// While a reduce label is active (try/finally), a shared node's reachability is
// context-dependent on the temporarily reduced label antecedents, so it must not
// be served from or written to the process-global cache (see #63583).
Comment on lines +28871 to +28873
if (!noCacheCheck && reduceLabelDepth === 0) {
const id = getFlowNodeId(flow);
const reachable = flowNodeReachable[id];
return reachable !== undefined ? reachable : (flowNodeReachable[id] = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ true));
Expand Down Expand Up @@ -28920,7 +28924,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const target = (flow as FlowReduceLabel).node.target;
const saveAntecedents = target.antecedent;
target.antecedent = (flow as FlowReduceLabel).node.antecedents;
// Suppress the shared reachability cache for the duration of the reduced walk:
// reachability computed under the reduced antecedents is not valid outside this
// context (and vice versa). Depth-counted to handle nested try/finally (see #63583).
reduceLabelDepth++;
const result = isReachableFlowNodeWorker((flow as FlowReduceLabel).antecedent, /*noCacheCheck*/ false);
reduceLabelDepth--;
target.antecedent = saveAntecedents;
return result;
Comment on lines 28924 to 28934
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
controlFlowSwitchReturnWithBranchingFinally.ts(32,9): error TS7027: Unreachable code detected.


==== controlFlowSwitchReturnWithBranchingFinally.ts (1 errors) ====
// https://github.com/microsoft/TypeScript/issues/63583
// A branch inside a `finally` block must not corrupt the reachability of the
// flow after the protected `try`. Here every `switch` case returns, so the
// function fully returns and the trailing code is genuinely unreachable —
// regardless of what the `finally` block does.

let y = false;

// Was wrongly: "Function lacks ending return statement..." because the branch
// in `finally` (the short-circuit of `||=`) polluted the shared reachability cache.
function test1(x: boolean): number {
try {
switch (x) {
case true: return 1;
case false: return 0;
}
}
finally { y ||= true; }
}

// test2 documents the *correct* counterpart: with an explicit trailing return,
// that return is genuinely unreachable (the switch is exhaustive on `boolean`),
// so reporting it is correct and finally-independent. Before the fix, test1 and
// test2 contradicted each other (test1 demanded this return; test2 then rejected
// it). After the fix they are consistent: test1 needs no return, test2's is dead.
function test2(x: boolean): number {
try {
switch (x) {
case true: return 1;
case false: return 0;
}
return 0; // correctly flagged unreachable
~~~~~~~~~
!!! error TS7027: Unreachable code detected.
}
finally { y ||= true; }
}

// A plain `if` (read with a branch) in `finally` triggered the same bug.
function test3(x: boolean): number {
try {
switch (x) {
case true: return 1;
case false: return 0;
}
}
finally { if (y) { } }
}

// Control: linear (non-branching) finally always worked.
function test4(x: boolean): number {
try {
switch (x) {
case true: return 1;
case false: return 0;
}
}
finally { y = true; }
}

// Nested try/finally with branching finally blocks (exercises depth counting).
function test5(x: boolean): number {
try {
try {
switch (x) {
case true: return 1;
case false: return 0;
}
}
finally { y ||= true; }
}
finally { if (y) { } }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//// [tests/cases/compiler/controlFlowSwitchReturnWithBranchingFinally.ts] ////

=== controlFlowSwitchReturnWithBranchingFinally.ts ===
// https://github.com/microsoft/TypeScript/issues/63583
// A branch inside a `finally` block must not corrupt the reachability of the
// flow after the protected `try`. Here every `switch` case returns, so the
// function fully returns and the trailing code is genuinely unreachable —
// regardless of what the `finally` block does.

let y = false;
>y : Symbol(y, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 6, 3))

// Was wrongly: "Function lacks ending return statement..." because the branch
// in `finally` (the short-circuit of `||=`) polluted the shared reachability cache.
function test1(x: boolean): number {
>test1 : Symbol(test1, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 6, 14))
>x : Symbol(x, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 10, 15))

try {
switch (x) {
>x : Symbol(x, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 10, 15))

case true: return 1;
case false: return 0;
}
}
finally { y ||= true; }
>y : Symbol(y, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 6, 3))
}

// test2 documents the *correct* counterpart: with an explicit trailing return,
// that return is genuinely unreachable (the switch is exhaustive on `boolean`),
// so reporting it is correct and finally-independent. Before the fix, test1 and
// test2 contradicted each other (test1 demanded this return; test2 then rejected
// it). After the fix they are consistent: test1 needs no return, test2's is dead.
function test2(x: boolean): number {
>test2 : Symbol(test2, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 18, 1))
>x : Symbol(x, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 25, 15))

try {
switch (x) {
>x : Symbol(x, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 25, 15))

case true: return 1;
case false: return 0;
}
return 0; // correctly flagged unreachable
}
finally { y ||= true; }
>y : Symbol(y, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 6, 3))
}

// A plain `if` (read with a branch) in `finally` triggered the same bug.
function test3(x: boolean): number {
>test3 : Symbol(test3, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 34, 1))
>x : Symbol(x, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 37, 15))

try {
switch (x) {
>x : Symbol(x, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 37, 15))

case true: return 1;
case false: return 0;
}
}
finally { if (y) { } }
>y : Symbol(y, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 6, 3))
}

// Control: linear (non-branching) finally always worked.
function test4(x: boolean): number {
>test4 : Symbol(test4, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 45, 1))
>x : Symbol(x, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 48, 15))

try {
switch (x) {
>x : Symbol(x, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 48, 15))

case true: return 1;
case false: return 0;
}
}
finally { y = true; }
>y : Symbol(y, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 6, 3))
}

// Nested try/finally with branching finally blocks (exercises depth counting).
function test5(x: boolean): number {
>test5 : Symbol(test5, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 56, 1))
>x : Symbol(x, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 59, 15))

try {
try {
switch (x) {
>x : Symbol(x, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 59, 15))

case true: return 1;
case false: return 0;
}
}
finally { y ||= true; }
>y : Symbol(y, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 6, 3))
}
finally { if (y) { } }
>y : Symbol(y, Decl(controlFlowSwitchReturnWithBranchingFinally.ts, 6, 3))
}

Loading