Skip to content

Fix reachability of code after a try with a branching finally (#63583)#63602

Closed
CedricConday wants to merge 1 commit into
microsoft:mainfrom
CedricConday:fix/finally-branch-reachability-63583
Closed

Fix reachability of code after a try with a branching finally (#63583)#63602
CedricConday wants to merge 1 commit into
microsoft:mainfrom
CedricConday:fix/finally-branch-reachability-63583

Conversation

@CedricConday

Copy link
Copy Markdown

Fixes #63583.

Problem

A function whose try body is an exhaustive switch that returns in every case is wrongly flagged Function lacks ending return statement — but only when the finally block contains a branch:

function test(x: boolean): number {
    try {
        switch (x) {
            case true: return 1;
            case false: return 0;
        }
    } finally { y ||= true; }   // branch in finally => bogus TS2366
}

The original report framed it as "read+write in finally", but the real trigger is any branch in the finally block (y ||= true, y = y || true, if (y) {} all reproduce; a linear y = true or a non-branching read does not).

Root cause

Reachability caches results for shared flow nodes in the process-global flowNodeReachable cache. While a ReduceLabel is active (try/finally), isReachableFlowNodeWorker temporarily rewrites the target label's antecedent, so the reachability of nodes reached during that walk is dependent on the reduced context. The ReduceLabel handler cleared only the single-entry lastFlowNode cache — not the shared cache. A branch inside finally produces a shared merge node whose reachability was cached under one reduce context (e.g. the return-exit antecedents) and then reused under another (the normal-exit antecedents), corrupting both end-of-function and unreachable-code analysis. With no branch there is no shared node in the finally, so the bug doesn't surface.

This mirrors the type-flow path, which already scopes its shared cache per invocation (sharedFlowStart/sharedFlowCount); reachability had no equivalent guard.

Fix

Suppress the shared reachability cache while a label reduction is active, depth-counted to handle nested try/finally. Reachability under reduced antecedents is simply recomputed rather than served from / written to the global cache.

Tests

Added controlFlowSwitchReturnWithBranchingFinally.ts covering the branching-finally cases (now clean), a non-branching control, and nested try/finally. The accompanying return-after-exhaustive-switch case is still correctly reported as unreachable (this is finally-independent — verified with no try/finally present), confirming the only defect was the contradiction. No baseline changes in the control-flow / finally / reachability / try-catch suites (816 passing).


Disclosure: I'm an AI engineer and use Claude Code in my workflow. The root-cause analysis and fix are mine and fully reviewed; happy to walk through any part.

…t#63583)

The reachability analysis caches results for shared flow nodes in a
process-global cache (flowNodeReachable). When a ReduceLabel is active
(try/finally), it temporarily rewrites the target label's antecedents,
making the reachability of nodes reached during that walk dependent on
the reduced context. The ReduceLabel handler cleared only the single
lastFlowNode cache, not the shared cache, so a branch inside a finally
block (a shared merge node) had its reachability cached under one
reduce context and reused under another, corrupting end-of-function and
unreachable-code analysis.

Suppress the shared reachability cache while a label reduction is
active, depth-counted for nested try/finally.
Copilot AI review requested due to automatic review settings June 30, 2026 14:54
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Jun 30, 2026
@typescript-automation typescript-automation Bot added the For Backlog Bug PRs that fix a backlog bug label Jun 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a control-flow reachability caching bug in the checker where reachability results for shared flow nodes could be incorrectly reused across different reduced-label contexts (e.g. try/finally), producing contradictory diagnostics like TS2366 (“Function lacks ending return statement”) when finally contains branching.

Changes:

  • Suppress the shared reachability cache for the duration of a FlowFlags.ReduceLabel walk (depth-counted for nested try/finally).
  • Add a new compiler regression test covering exhaustive switch returns with branching finally (including a nested case and a non-branching control).
  • Add new reference baselines for the test (types, symbols, and expected error output).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/compiler/checker.ts Disables shared reachability caching while reduce-label rewriting is active to avoid context-dependent cache pollution.
tests/cases/compiler/controlFlowSwitchReturnWithBranchingFinally.ts New regression test covering branching-in-finally cases (and a non-branching control).
tests/baselines/reference/controlFlowSwitchReturnWithBranchingFinally.types Baseline for type display/output of the new test.
tests/baselines/reference/controlFlowSwitchReturnWithBranchingFinally.symbols Baseline for symbol output of the new test.
tests/baselines/reference/controlFlowSwitchReturnWithBranchingFinally.errors.txt Baseline verifying only the intended unreachable-code error is reported.

Comment thread src/compiler/checker.ts
Comment on lines +28871 to +28873
// 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 thread src/compiler/checker.ts
Comment on lines 28924 to 28934
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;
@MartinJohns

Copy link
Copy Markdown
Contributor

I'm an AI engineer and use Claude Code in my workflow.

Claude failed to read this: https://github.com/microsoft/TypeScript/blob/main/CLAUDE.md

@CedricConday

Copy link
Copy Markdown
Author

Thanks all, and thanks @MartinJohns for the pointer — you're right, I missed AGENTS.md. This repo is in maintenance mode and isn't accepting general bug-fix PRs against the JS codebase (active work has moved to microsoft/typescript-go), so this change is out of scope here. Closing accordingly. Happy to re-target the fix at typescript-go if it'd still be useful there. Apologies for the noise.

@github-project-automation github-project-automation Bot moved this from Not started to Done in PR Backlog Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Backlog Bug PRs that fix a backlog bug

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Finally is not implemented correctly in all cases

3 participants