Fix reachability of code after a try with a branching finally (#63583)#63602
Fix reachability of code after a try with a branching finally (#63583)#63602CedricConday wants to merge 1 commit into
Conversation
…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.
There was a problem hiding this comment.
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.ReduceLabelwalk (depth-counted for nestedtry/finally). - Add a new compiler regression test covering exhaustive
switchreturns with branchingfinally(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. |
| // 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). |
| 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; |
Claude failed to read this: https://github.com/microsoft/TypeScript/blob/main/CLAUDE.md |
|
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. |
Fixes #63583.
Problem
A function whose
trybody is an exhaustiveswitchthat returns in every case is wrongly flaggedFunction lacks ending return statement— but only when thefinallyblock contains a branch:The original report framed it as "read+write in
finally", but the real trigger is any branch in thefinallyblock (y ||= true,y = y || true,if (y) {}all reproduce; a lineary = trueor a non-branching read does not).Root cause
Reachability caches results for shared flow nodes in the process-global
flowNodeReachablecache. While aReduceLabelis active (try/finally),isReachableFlowNodeWorkertemporarily rewrites the target label'santecedent, so the reachability of nodes reached during that walk is dependent on the reduced context. TheReduceLabelhandler cleared only the single-entrylastFlowNodecache — not the shared cache. A branch insidefinallyproduces 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 thefinally, 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.tscovering the branching-finally cases (now clean), a non-branching control, and nested try/finally. The accompanyingreturn-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.