From af1fc6ed3266d697600a5a5192e59b2339d96f4f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:51:32 -0800 Subject: [PATCH 01/18] Move dead code checks to checker --- internal/ast/ast.go | 5 +- internal/binder/binder.go | 130 ++++--------------------------- internal/checker/checker.go | 48 ++++++++++-- internal/core/compileroptions.go | 10 +-- 4 files changed, 64 insertions(+), 129 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 72ee46c32f..cec4a4689c 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -3564,8 +3564,9 @@ func (node *DebuggerStatement) Clone(f NodeFactoryCoercible) *Node { type LabeledStatement struct { StatementBase - Label *IdentifierNode // IdentifierNode - Statement *Statement // Statement + Label *IdentifierNode // IdentifierNode + Statement *Statement // Statement + IsReferenced bool // Set by binder to indicate if the label is used by a break or continue statement } func (f *NodeFactory) NewLabeledStatement(label *IdentifierNode, statement *Statement) *Node { diff --git a/internal/binder/binder.go b/internal/binder/binder.go index e7409cea4e..7387a3d292 100644 --- a/internal/binder/binder.go +++ b/internal/binder/binder.go @@ -42,10 +42,9 @@ const ( ) type Binder struct { - file *ast.SourceFile - bindFunc func(*ast.Node) bool - unreachableFlow *ast.FlowNode - reportedUnreachableFlow *ast.FlowNode + file *ast.SourceFile + bindFunc func(*ast.Node) bool + unreachableFlow *ast.FlowNode container *ast.Node thisContainer *ast.Node @@ -122,7 +121,6 @@ func bindSourceFile(file *ast.SourceFile) { b.file = file b.inStrictMode = b.options().BindInStrictMode && !file.IsDeclarationFile || ast.IsExternalModule(file) b.unreachableFlow = b.newFlowNode(ast.FlowFlagsUnreachable) - b.reportedUnreachableFlow = b.newFlowNode(ast.FlowFlagsUnreachable) b.bind(file.AsNode()) file.SymbolCount = b.symbolCount file.ClassifiableNames = b.classifiableNames @@ -1535,18 +1533,23 @@ func (b *Binder) bindChildren(node *ast.Node) { // Most nodes aren't valid in an assignment pattern, so we clear the value here // and set it before we descend into nodes that could actually be part of an assignment pattern. b.inAssignmentPattern = false - if b.checkUnreachable(node) { - b.bindEachChild(node) - b.inAssignmentPattern = saveInAssignmentPattern - return - } + kind := node.Kind - if kind >= ast.KindFirstStatement && kind <= ast.KindLastStatement && (b.options().AllowUnreachableCode != core.TSTrue || kind == ast.KindReturnStatement) { + // Set the flow node so the checker can determine reachability + // Set flow node data BEFORE checking unreachability, because we return early for unreachable nodes + // Set on: statements, class/enum/module declarations (but not function declarations which are hoisted) + if (ast.KindFirstStatement <= kind && kind <= ast.KindLastStatement) || ast.IsClassDeclaration(node) || ast.IsEnumDeclaration(node) || ast.IsModuleDeclaration(node) { hasFlowNodeData := node.FlowNodeData() if hasFlowNodeData != nil { hasFlowNodeData.FlowNode = b.currentFlow } } + + if b.currentFlow.Flags&ast.FlowFlagsUnreachable != 0 { + b.bindEachChild(node) + b.inAssignmentPattern = saveInAssignmentPattern + return + } switch node.Kind { case ast.KindWhileStatement: b.bindWhileStatement(node) @@ -1657,94 +1660,6 @@ func (b *Binder) bindEachStatementFunctionsFirst(statements *ast.NodeList) { } } -func (b *Binder) checkUnreachable(node *ast.Node) bool { - if b.currentFlow.Flags&ast.FlowFlagsUnreachable == 0 { - return false - } - if b.currentFlow == b.unreachableFlow { - // report errors on all statements except empty ones - // report errors on class declarations - // report errors on enums with preserved emit - // report errors on instantiated modules - reportError := ast.IsStatementButNotDeclaration(node) && !ast.IsEmptyStatement(node) || - ast.IsClassDeclaration(node) || - isEnumDeclarationWithPreservedEmit(node, b.options()) || - ast.IsModuleDeclaration(node) && b.shouldReportErrorOnModuleDeclaration(node) - if reportError { - b.currentFlow = b.reportedUnreachableFlow - if b.options().AllowUnreachableCode != core.TSTrue { - // unreachable code is reported if - // - user has explicitly asked about it AND - // - statement is in not ambient context (statements in ambient context is already an error - // so we should not report extras) AND - // - node is not variable statement OR - // - node is block scoped variable statement OR - // - node is not block scoped variable statement and at least one variable declaration has initializer - // Rationale: we don't want to report errors on non-initialized var's since they are hoisted - // On the other side we do want to report errors on non-initialized 'lets' because of TDZ - isError := unreachableCodeIsError(b.options()) && node.Flags&ast.NodeFlagsAmbient == 0 && (!ast.IsVariableStatement(node) || - ast.GetCombinedNodeFlags(node.AsVariableStatement().DeclarationList)&ast.NodeFlagsBlockScoped != 0 || - core.Some(node.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool { - return d.Initializer() != nil - })) - b.errorOnEachUnreachableRange(node, isError) - } - } - } - return true -} - -func (b *Binder) shouldReportErrorOnModuleDeclaration(node *ast.Node) bool { - instanceState := ast.GetModuleInstanceState(node) - return instanceState == ast.ModuleInstanceStateInstantiated || (instanceState == ast.ModuleInstanceStateConstEnumOnly && b.options().ShouldPreserveConstEnums) -} - -func (b *Binder) errorOnEachUnreachableRange(node *ast.Node, isError bool) { - if b.isExecutableStatement(node) && ast.IsBlock(node.Parent) { - statements := node.Parent.Statements() - index := slices.Index(statements, node) - var first, last *ast.Node - for _, s := range statements[index:] { - if b.isExecutableStatement(s) { - if first == nil { - first = s - } - last = s - } else if first != nil { - b.errorOrSuggestionOnRange(isError, first, last, diagnostics.Unreachable_code_detected) - first = nil - } - } - if first != nil { - b.errorOrSuggestionOnRange(isError, first, last, diagnostics.Unreachable_code_detected) - } - } else { - b.errorOrSuggestionOnNode(isError, node, diagnostics.Unreachable_code_detected) - } -} - -// As opposed to a pure declaration like an `interface` -func (b *Binder) isExecutableStatement(s *ast.Node) bool { - // Don't remove statements that can validly be used before they appear. - return !ast.IsFunctionDeclaration(s) && !b.isPurelyTypeDeclaration(s) && !(ast.IsVariableStatement(s) && ast.GetCombinedNodeFlags(s)&ast.NodeFlagsBlockScoped == 0 && - core.Some(s.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool { - return d.Initializer() == nil - })) -} - -func (b *Binder) isPurelyTypeDeclaration(s *ast.Node) bool { - switch s.Kind { - case ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration: - return true - case ast.KindModuleDeclaration: - return ast.GetModuleInstanceState(s) != ast.ModuleInstanceStateInstantiated - case ast.KindEnumDeclaration: - return !isEnumDeclarationWithPreservedEmit(s, b.options()) - default: - return false - } -} - func (b *Binder) setContinueTarget(node *ast.Node, target *ast.FlowLabel) *ast.FlowLabel { label := b.activeLabelList for label != nil && node.Parent.Kind == ast.KindLabeledStatement { @@ -2131,9 +2046,8 @@ func (b *Binder) bindLabeledStatement(node *ast.Node) { } b.bind(stmt.Label) b.bind(stmt.Statement) - if !b.activeLabelList.referenced && b.options().AllowUnusedLabels != core.TSTrue { - b.errorOrSuggestionOnNode(unusedLabelIsError(b.options()), stmt.Label, diagnostics.Unused_label) - } + // Store whether the label was referenced so the checker can report unused labels later + stmt.IsReferenced = b.activeLabelList.referenced b.activeLabelList = b.activeLabelList.next b.addAntecedent(postStatementLabel, b.currentFlow) b.currentFlow = b.finishFlowLabel(postStatementLabel) @@ -2454,10 +2368,6 @@ func (b *Binder) bindInitializer(node *ast.Node) { b.currentFlow = b.finishFlowLabel(exitFlow) } -func isEnumDeclarationWithPreservedEmit(node *ast.Node, options core.SourceFileAffectingCompilerOptions) bool { - return node.Kind == ast.KindEnumDeclaration && (!ast.IsEnumConst(node) || options.ShouldPreserveConstEnums) -} - func setFlowNode(node *ast.Node, flowNode *ast.FlowNode) { data := node.FlowNodeData() if data != nil { @@ -2749,14 +2659,6 @@ func isFunctionSymbol(symbol *ast.Symbol) bool { return false } -func unreachableCodeIsError(options core.SourceFileAffectingCompilerOptions) bool { - return options.AllowUnreachableCode == core.TSFalse -} - -func unusedLabelIsError(options core.SourceFileAffectingCompilerOptions) bool { - return options.AllowUnusedLabels == core.TSFalse -} - func isStatementCondition(node *ast.Node) bool { switch node.Parent.Kind { case ast.KindIfStatement, ast.KindWhileStatement, ast.KindDoStatement: diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 5b6fde57ec..6df17323c2 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2177,11 +2177,36 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) { } } } - kind := node.Kind - if kind >= ast.KindFirstStatement && kind <= ast.KindLastStatement { - flowNode := node.FlowNodeData().FlowNode - if flowNode != nil && !c.isReachableFlowNode(flowNode) { - c.errorOrSuggestion(c.compilerOptions.AllowUnreachableCode == core.TSFalse, node, diagnostics.Unreachable_code_detected) + // Check unreachable code - any node with FlowNodeData can be checked + flowNode := node.FlowNodeData() + if flowNode != nil && flowNode.FlowNode != nil && !c.isReachableFlowNode(flowNode.FlowNode) { + // report errors on all statements except empty ones + // report errors on class declarations + // report errors on enums with preserved emit + // report errors on instantiated modules + reportError := ast.IsStatementButNotDeclaration(node) && !ast.IsEmptyStatement(node) || + ast.IsClassDeclaration(node) || + isEnumDeclarationWithPreservedEmit(node, c.compilerOptions) || + ast.IsModuleDeclaration(node) && shouldReportErrorOnModuleDeclaration(node, c.compilerOptions) + if reportError && c.compilerOptions.AllowUnreachableCode != core.TSTrue { + // unreachable code is reported if + // - user has explicitly asked about it AND + // - statement is in not ambient context (statements in ambient context is already an error + // so we should not report extras) AND + // - node is not variable statement OR + // - node is block scoped variable statement OR + // - node is not block scoped variable statement and at least one variable declaration has initializer + // Rationale: we don't want to report errors on non-initialized var's since they are hoisted + // On the other side we do want to report errors on non-initialized 'lets' because of TDZ + isError := c.compilerOptions.AllowUnreachableCode == core.TSFalse && node.Flags&ast.NodeFlagsAmbient == 0 && (!ast.IsVariableStatement(node) || + ast.GetCombinedNodeFlags(node.AsVariableStatement().DeclarationList)&ast.NodeFlagsBlockScoped != 0 || + core.Some(node.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool { + return d.Initializer() != nil + })) + sourceFile := ast.GetSourceFileOfNode(node) + textRange := core.NewTextRange(scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos()).Pos(), node.End()) + diagnostic := ast.NewDiagnostic(sourceFile, textRange, diagnostics.Unreachable_code_detected) + c.addErrorOrSuggestion(isError, diagnostic) } } switch node.Kind { @@ -2306,6 +2331,15 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) { } } +func isEnumDeclarationWithPreservedEmit(node *ast.Node, options *core.CompilerOptions) bool { + return ast.IsEnumDeclaration(node) && (!ast.IsEnumConst(node) || options.ShouldPreserveConstEnums()) +} + +func shouldReportErrorOnModuleDeclaration(node *ast.Node, options *core.CompilerOptions) bool { + instanceState := ast.GetModuleInstanceState(node) + return instanceState == ast.ModuleInstanceStateInstantiated || (instanceState == ast.ModuleInstanceStateConstEnumOnly && options.ShouldPreserveConstEnums()) +} + // Function and class expression bodies are checked after all statements in the enclosing body. This is // to ensure constructs like the following are permitted: // @@ -4020,6 +4054,10 @@ func (c *Checker) checkLabeledStatement(node *ast.Node) { } } } + // Check for unused labels + if !labeledStatement.IsReferenced && c.compilerOptions.AllowUnusedLabels != core.TSTrue { + c.errorOrSuggestion(c.compilerOptions.AllowUnusedLabels == core.TSFalse, labelNode, diagnostics.Unused_label) + } c.checkSourceElement(labeledStatement.Statement) } diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index 2f41f64dd6..057d034ec2 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -365,19 +365,13 @@ func (options *CompilerOptions) GetPathsBasePath(currentDirectory string) string // SourceFileAffectingCompilerOptions are the precomputed CompilerOptions values which // affect the parse and bind of a source file. type SourceFileAffectingCompilerOptions struct { - AllowUnreachableCode Tristate - AllowUnusedLabels Tristate - BindInStrictMode bool - ShouldPreserveConstEnums bool + BindInStrictMode bool } func (options *CompilerOptions) SourceFileAffecting() SourceFileAffectingCompilerOptions { options.sourceFileAffectingCompilerOptionsOnce.Do(func() { options.sourceFileAffectingCompilerOptions = SourceFileAffectingCompilerOptions{ - AllowUnreachableCode: options.AllowUnreachableCode, - AllowUnusedLabels: options.AllowUnusedLabels, - BindInStrictMode: options.AlwaysStrict.IsTrue() || options.Strict.IsTrue(), - ShouldPreserveConstEnums: options.ShouldPreserveConstEnums(), + BindInStrictMode: options.AlwaysStrict.IsTrue() || options.Strict.IsTrue(), } }) return options.sourceFileAffectingCompilerOptions From a6d1552bdacfb962eae2fe7651a764cb84acddb2 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 8 Nov 2025 18:40:59 -0800 Subject: [PATCH 02/18] Fix up ranges --- internal/checker/checker.go | 99 ++++++++++++--- .../compiler/reachabilityChecks1.errors.txt | 45 ++++++- .../reachabilityChecks1.errors.txt.diff | 116 ++++++++++++++++++ .../neverReturningFunctions1.errors.txt | 8 +- .../neverReturningFunctions1.errors.txt.diff | 32 +++++ 5 files changed, 276 insertions(+), 24 deletions(-) create mode 100644 testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff create mode 100644 testdata/baselines/reference/submodule/conformance/neverReturningFunctions1.errors.txt.diff diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 6df17323c2..8f1f0bfe9b 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -774,6 +774,7 @@ type Checker struct { lastFlowNodeReachable bool flowNodeReachable map[*ast.FlowNode]bool flowNodePostSuper map[*ast.FlowNode]bool + reportedUnreachableStatements collections.Set[*ast.Node] renamedBindingElementsInTypes []*ast.Node contextualInfos []ContextualInfo inferenceContextInfos []InferenceContextInfo @@ -2189,24 +2190,24 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) { isEnumDeclarationWithPreservedEmit(node, c.compilerOptions) || ast.IsModuleDeclaration(node) && shouldReportErrorOnModuleDeclaration(node, c.compilerOptions) if reportError && c.compilerOptions.AllowUnreachableCode != core.TSTrue { - // unreachable code is reported if - // - user has explicitly asked about it AND - // - statement is in not ambient context (statements in ambient context is already an error - // so we should not report extras) AND - // - node is not variable statement OR - // - node is block scoped variable statement OR - // - node is not block scoped variable statement and at least one variable declaration has initializer - // Rationale: we don't want to report errors on non-initialized var's since they are hoisted - // On the other side we do want to report errors on non-initialized 'lets' because of TDZ - isError := c.compilerOptions.AllowUnreachableCode == core.TSFalse && node.Flags&ast.NodeFlagsAmbient == 0 && (!ast.IsVariableStatement(node) || - ast.GetCombinedNodeFlags(node.AsVariableStatement().DeclarationList)&ast.NodeFlagsBlockScoped != 0 || - core.Some(node.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool { - return d.Initializer() != nil - })) - sourceFile := ast.GetSourceFileOfNode(node) - textRange := core.NewTextRange(scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos()).Pos(), node.End()) - diagnostic := ast.NewDiagnostic(sourceFile, textRange, diagnostics.Unreachable_code_detected) - c.addErrorOrSuggestion(isError, diagnostic) + // Only report if we haven't already reported this statement + if !c.reportedUnreachableStatements.Has(node) { + // unreachable code is reported if + // - user has explicitly asked about it AND + // - statement is in not ambient context (statements in ambient context is already an error + // so we should not report extras) AND + // - node is not variable statement OR + // - node is block scoped variable statement OR + // - node is not block scoped variable statement and at least one variable declaration has initializer + // Rationale: we don't want to report errors on non-initialized var's since they are hoisted + // On the other side we do want to report errors on non-initialized 'lets' because of TDZ + isError := c.compilerOptions.AllowUnreachableCode == core.TSFalse && node.Flags&ast.NodeFlagsAmbient == 0 && (!ast.IsVariableStatement(node) || + ast.GetCombinedNodeFlags(node.AsVariableStatement().DeclarationList)&ast.NodeFlagsBlockScoped != 0 || + core.Some(node.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool { + return d.Initializer() != nil + })) + c.errorOnEachUnreachableRange(node, isError) + } } } switch node.Kind { @@ -2340,6 +2341,68 @@ func shouldReportErrorOnModuleDeclaration(node *ast.Node, options *core.Compiler return instanceState == ast.ModuleInstanceStateInstantiated || (instanceState == ast.ModuleInstanceStateConstEnumOnly && options.ShouldPreserveConstEnums()) } +func (c *Checker) errorOnEachUnreachableRange(node *ast.Node, isError bool) { + errorOrSuggestion := func(first *ast.Node, last *ast.Node) { + sourceFile := ast.GetSourceFileOfNode(first) + textRange := core.NewTextRange(scanner.GetRangeOfTokenAtPosition(sourceFile, first.Pos()).Pos(), last.End()) + diagnostic := ast.NewDiagnostic(sourceFile, textRange, diagnostics.Unreachable_code_detected) + c.addErrorOrSuggestion(isError, diagnostic) + } + + markRangeAsReported := func(first *ast.Node, last *ast.Node, statements []*ast.Node) { + for _, markedNode := range statements[slices.Index(statements, first) : slices.Index(statements, last)+1] { + c.reportedUnreachableStatements.Add(markedNode) + } + } + + isPurelyTypeDeclaration := func(s *ast.Node) bool { + switch s.Kind { + case ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration: + return true + case ast.KindModuleDeclaration: + return ast.GetModuleInstanceState(s) != ast.ModuleInstanceStateInstantiated + case ast.KindEnumDeclaration: + return ast.HasSyntacticModifier(s, ast.ModifierFlagsConst) && !c.compilerOptions.ShouldPreserveConstEnums() + default: + return false + } + } + + isExecutableStatement := func(s *ast.Node) bool { + // Don't remove statements that can validly be used before they appear. + // This includes function declarations (which are hoisted), type declarations, and uninitialized vars. + return !isPurelyTypeDeclaration(s) && + !ast.IsFunctionDeclaration(s) && + !(ast.IsVariableStatement(s) && (ast.GetCombinedNodeFlags(s.AsVariableStatement().DeclarationList)&ast.NodeFlagsBlockScoped == 0) && + core.Every(s.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool { + return d.AsVariableDeclaration().Initializer == nil + })) + } + + if isExecutableStatement(node) && ast.IsBlock(node.Parent) { + statements := node.Parent.AsBlock().Statements.Nodes + index := slices.Index(statements, node) + var first, last *ast.Node + for _, s := range statements[index:] { + if isExecutableStatement(s) { + if first == nil { + first = s + } + last = s + } else { + // Stop scanning when we hit a non-executable statement + break + } + } + if first != nil { + errorOrSuggestion(first, last) + markRangeAsReported(first, last, statements) + } + } else { + errorOrSuggestion(node, node) + } +} + // Function and class expression bodies are checked after all statements in the enclosing body. This is // to ensure constructs like the following are permitted: // diff --git a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt index d25977a05b..c129870322 100644 --- a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt +++ b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt @@ -1,58 +1,95 @@ reachabilityChecks1.ts(2,1): error TS7027: Unreachable code detected. +reachabilityChecks1.ts(4,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(6,5): error TS7027: Unreachable code detected. +reachabilityChecks1.ts(9,1): error TS7027: Unreachable code detected. +reachabilityChecks1.ts(16,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(18,5): error TS7027: Unreachable code detected. +reachabilityChecks1.ts(23,1): error TS7027: Unreachable code detected. +reachabilityChecks1.ts(28,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(30,5): error TS7027: Unreachable code detected. reachabilityChecks1.ts(47,5): error TS7027: Unreachable code detected. +reachabilityChecks1.ts(51,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(60,5): error TS7027: Unreachable code detected. reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. -==== reachabilityChecks1.ts (7 errors) ==== +==== reachabilityChecks1.ts (13 errors) ==== while (true); var x = 1; ~~~~~~~~~~ !!! error TS7027: Unreachable code detected. module A { + ~~~~~~~~~~ while (true); + ~~~~~~~~~~~~~~~~~ let x; + ~~~~~~~~~~ ~~~~~~ !!! error TS7027: Unreachable code detected. } + ~ +!!! error TS7027: Unreachable code detected. module A1 { + ~~~~~~~~~~~ do {} while(true); + ~~~~~~~~~~~~~~~~~~~~~~ module A { + ~~~~~~~~~~~~~~ interface F {} + ~~~~~~~~~~~~~~~~~~~~~~ } + ~~~~~ } + ~ +!!! error TS7027: Unreachable code detected. module A2 { + ~~~~~~~~~~~ while (true); + ~~~~~~~~~~~~~~~~~ module A { + ~~~~~~~~~~~~~~ ~~~~~~~~~~ var x = 1; + ~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~ } ~~~~~ + ~~~~~ !!! error TS7027: Unreachable code detected. } + ~ +!!! error TS7027: Unreachable code detected. module A3 { + ~~~~~~~~~~~ while (true); + ~~~~~~~~~~~~~~~~~ type T = string; + ~~~~~~~~~~~~~~~~~~~~ } + ~ +!!! error TS7027: Unreachable code detected. module A4 { + ~~~~~~~~~~~ while (true); + ~~~~~~~~~~~~~~~~~ module A { + ~~~~~~~~~~~~~~ ~~~~~~~~~~ const enum E { X } + ~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~ } ~~~~~ + ~~~~~ !!! error TS7027: Unreachable code detected. } + ~ +!!! error TS7027: Unreachable code detected. function f1(x) { if (x) { @@ -74,10 +111,16 @@ reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. } module B { + ~~~~~~~~~~ for (; ;); + ~~~~~~~~~~~~~~ module C { + ~~~~~~~~~~~~~~ } + ~~~~~ } + ~ +!!! error TS7027: Unreachable code detected. function f3() { do { diff --git a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff new file mode 100644 index 0000000000..de5c1b581b --- /dev/null +++ b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff @@ -0,0 +1,116 @@ +--- old.reachabilityChecks1.errors.txt ++++ new.reachabilityChecks1.errors.txt +@@= skipped -0, +0 lines =@@ + reachabilityChecks1.ts(2,1): error TS7027: Unreachable code detected. ++reachabilityChecks1.ts(4,1): error TS7027: Unreachable code detected. + reachabilityChecks1.ts(6,5): error TS7027: Unreachable code detected. ++reachabilityChecks1.ts(9,1): error TS7027: Unreachable code detected. ++reachabilityChecks1.ts(16,1): error TS7027: Unreachable code detected. + reachabilityChecks1.ts(18,5): error TS7027: Unreachable code detected. ++reachabilityChecks1.ts(23,1): error TS7027: Unreachable code detected. ++reachabilityChecks1.ts(28,1): error TS7027: Unreachable code detected. + reachabilityChecks1.ts(30,5): error TS7027: Unreachable code detected. + reachabilityChecks1.ts(47,5): error TS7027: Unreachable code detected. ++reachabilityChecks1.ts(51,1): error TS7027: Unreachable code detected. + reachabilityChecks1.ts(60,5): error TS7027: Unreachable code detected. + reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. + + +-==== reachabilityChecks1.ts (7 errors) ==== ++==== reachabilityChecks1.ts (13 errors) ==== + while (true); + var x = 1; + ~~~~~~~~~~ + !!! error TS7027: Unreachable code detected. + + module A { ++ ~~~~~~~~~~ + while (true); ++ ~~~~~~~~~~~~~~~~~ + let x; ++ ~~~~~~~~~~ + ~~~~~~ + !!! error TS7027: Unreachable code detected. + } ++ ~ ++!!! error TS7027: Unreachable code detected. + + module A1 { ++ ~~~~~~~~~~~ + do {} while(true); ++ ~~~~~~~~~~~~~~~~~~~~~~ + module A { ++ ~~~~~~~~~~~~~~ + interface F {} ++ ~~~~~~~~~~~~~~~~~~~~~~ + } ++ ~~~~~ + } ++ ~ ++!!! error TS7027: Unreachable code detected. + + module A2 { ++ ~~~~~~~~~~~ + while (true); ++ ~~~~~~~~~~~~~~~~~ + module A { ++ ~~~~~~~~~~~~~~ + ~~~~~~~~~~ + var x = 1; + ~~~~~~~~~~~~~~~~~~ ++ ~~~~~~~~~~~~~~~~~~ + } + ~~~~~ ++ ~~~~~ + !!! error TS7027: Unreachable code detected. + } ++ ~ ++!!! error TS7027: Unreachable code detected. + + module A3 { ++ ~~~~~~~~~~~ + while (true); ++ ~~~~~~~~~~~~~~~~~ + type T = string; ++ ~~~~~~~~~~~~~~~~~~~~ + } ++ ~ ++!!! error TS7027: Unreachable code detected. + + module A4 { ++ ~~~~~~~~~~~ + while (true); ++ ~~~~~~~~~~~~~~~~~ + module A { ++ ~~~~~~~~~~~~~~ + ~~~~~~~~~~ + const enum E { X } + ~~~~~~~~~~~~~~~~~~~~~~~~~~ ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~ + } + ~~~~~ ++ ~~~~~ + !!! error TS7027: Unreachable code detected. + } ++ ~ ++!!! error TS7027: Unreachable code detected. + + function f1(x) { + if (x) { +@@= skipped -73, +110 lines =@@ + } + + module B { ++ ~~~~~~~~~~ + for (; ;); ++ ~~~~~~~~~~~~~~ + module C { ++ ~~~~~~~~~~~~~~ + } ++ ~~~~~ + } ++ ~ ++!!! error TS7027: Unreachable code detected. + + function f3() { + do { \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/neverReturningFunctions1.errors.txt b/testdata/baselines/reference/submodule/conformance/neverReturningFunctions1.errors.txt index 8a7a883714..a15d15e17d 100644 --- a/testdata/baselines/reference/submodule/conformance/neverReturningFunctions1.errors.txt +++ b/testdata/baselines/reference/submodule/conformance/neverReturningFunctions1.errors.txt @@ -13,7 +13,6 @@ neverReturningFunctions1.ts(101,13): error TS7027: Unreachable code detected. neverReturningFunctions1.ts(103,9): error TS7027: Unreachable code detected. neverReturningFunctions1.ts(105,5): error TS7027: Unreachable code detected. neverReturningFunctions1.ts(111,9): error TS7027: Unreachable code detected. -neverReturningFunctions1.ts(112,9): error TS7027: Unreachable code detected. neverReturningFunctions1.ts(122,9): error TS7027: Unreachable code detected. neverReturningFunctions1.ts(127,9): error TS7027: Unreachable code detected. neverReturningFunctions1.ts(129,5): error TS7027: Unreachable code detected. @@ -23,7 +22,7 @@ neverReturningFunctions1.ts(148,9): error TS7027: Unreachable code detected. neverReturningFunctions1.ts(153,5): error TS7027: Unreachable code detected. -==== neverReturningFunctions1.ts (23 errors) ==== +==== neverReturningFunctions1.ts (22 errors) ==== function fail(message?: string): never { throw new Error(message); } @@ -163,10 +162,9 @@ neverReturningFunctions1.ts(153,5): error TS7027: Unreachable code detected. if (typeof x.a === "string") { fail(); x; // Unreachable - ~~ -!!! error TS7027: Unreachable code detected. + ~~~~~~~~~~~~~~~~~~~~ x.a; // Unreachable - ~~~~ + ~~~~~~~~~~~~ !!! error TS7027: Unreachable code detected. } x; // { a: string | number } diff --git a/testdata/baselines/reference/submodule/conformance/neverReturningFunctions1.errors.txt.diff b/testdata/baselines/reference/submodule/conformance/neverReturningFunctions1.errors.txt.diff new file mode 100644 index 0000000000..3f7c033e1c --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/neverReturningFunctions1.errors.txt.diff @@ -0,0 +1,32 @@ +--- old.neverReturningFunctions1.errors.txt ++++ new.neverReturningFunctions1.errors.txt +@@= skipped -12, +12 lines =@@ + neverReturningFunctions1.ts(103,9): error TS7027: Unreachable code detected. + neverReturningFunctions1.ts(105,5): error TS7027: Unreachable code detected. + neverReturningFunctions1.ts(111,9): error TS7027: Unreachable code detected. +-neverReturningFunctions1.ts(112,9): error TS7027: Unreachable code detected. + neverReturningFunctions1.ts(122,9): error TS7027: Unreachable code detected. + neverReturningFunctions1.ts(127,9): error TS7027: Unreachable code detected. + neverReturningFunctions1.ts(129,5): error TS7027: Unreachable code detected. +@@= skipped -10, +9 lines =@@ + neverReturningFunctions1.ts(153,5): error TS7027: Unreachable code detected. + + +-==== neverReturningFunctions1.ts (23 errors) ==== ++==== neverReturningFunctions1.ts (22 errors) ==== + function fail(message?: string): never { + throw new Error(message); + } +@@= skipped -140, +140 lines =@@ + if (typeof x.a === "string") { + fail(); + x; // Unreachable +- ~~ +-!!! error TS7027: Unreachable code detected. ++ ~~~~~~~~~~~~~~~~~~~~ + x.a; // Unreachable +- ~~~~ ++ ~~~~~~~~~~~~ + !!! error TS7027: Unreachable code detected. + } + x; // { a: string | number } \ No newline at end of file From f792d48eaac0b88f82f0ccbb8673ccfb1e062c19 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:39:03 -0800 Subject: [PATCH 03/18] Sort of simplify --- internal/checker/checker.go | 47 ++++++- .../compiler/reachabilityChecks1.errors.txt | 45 +------ .../reachabilityChecks1.errors.txt.diff | 116 ------------------ 3 files changed, 43 insertions(+), 165 deletions(-) delete mode 100644 testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 8f1f0bfe9b..07b67c2833 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2379,9 +2379,7 @@ func (c *Checker) errorOnEachUnreachableRange(node *ast.Node, isError bool) { })) } - if isExecutableStatement(node) && ast.IsBlock(node.Parent) { - statements := node.Parent.AsBlock().Statements.Nodes - index := slices.Index(statements, node) + scanAndReportExecutableStatements := func(statements []*ast.Node, index int) { var first, last *ast.Node for _, s := range statements[index:] { if isExecutableStatement(s) { @@ -2390,7 +2388,6 @@ func (c *Checker) errorOnEachUnreachableRange(node *ast.Node, isError bool) { } last = s } else { - // Stop scanning when we hit a non-executable statement break } } @@ -2398,9 +2395,49 @@ func (c *Checker) errorOnEachUnreachableRange(node *ast.Node, isError bool) { errorOrSuggestion(first, last) markRangeAsReported(first, last, statements) } - } else { + } + + // Report unreachable code in blocks (function bodies, if/else blocks, etc.) + // and module blocks (the body of a module declaration). + // These scan forward to report ranges of consecutive unreachable statements. + if ast.IsBlock(node.Parent) && isExecutableStatement(node) { + scanAndReportExecutableStatements(node.Parent.AsBlock().Statements.Nodes, slices.Index(node.Parent.AsBlock().Statements.Nodes, node)) + return + } + if ast.IsModuleBlock(node.Parent) && isExecutableStatement(node) { + scanAndReportExecutableStatements(node.Parent.AsModuleBlock().Statements.Nodes, slices.Index(node.Parent.AsModuleBlock().Statements.Nodes, node)) + return + } + + // Top-level module declarations are never reported individually. + // Their contents are checked when the module body is visited. + // However, if there's no other unreachable code before them and no function declarations + // (which are hoisted), then report them to avoid silent unreachable modules. + if ast.IsModuleDeclaration(node) && ast.IsSourceFile(node.Parent) && shouldReportErrorOnModuleDeclaration(node, c.compilerOptions) { + if c.reportedUnreachableStatements.Has(node) { + return + } + statements := node.Parent.AsSourceFile().Statements.Nodes + index := slices.Index(statements, node) + // Don't report if there's a preceding function (hoisting makes flow complex) + // or other unreachable code (which would already be reported). + for i := range index { + s := statements[i] + if ast.IsFunctionDeclaration(s) { + return + } + if isExecutableStatement(s) && !ast.IsModuleDeclaration(s) { + if flowData := s.FlowNodeData(); flowData != nil && flowData.FlowNode != nil && !c.isReachableFlowNode(flowData.FlowNode) { + return + } + } + } errorOrSuggestion(node, node) + return } + + // Default: report the node individually + errorOrSuggestion(node, node) } // Function and class expression bodies are checked after all statements in the enclosing body. This is diff --git a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt index c129870322..d25977a05b 100644 --- a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt +++ b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt @@ -1,95 +1,58 @@ reachabilityChecks1.ts(2,1): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(4,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(6,5): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(9,1): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(16,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(18,5): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(23,1): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(28,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(30,5): error TS7027: Unreachable code detected. reachabilityChecks1.ts(47,5): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(51,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(60,5): error TS7027: Unreachable code detected. reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. -==== reachabilityChecks1.ts (13 errors) ==== +==== reachabilityChecks1.ts (7 errors) ==== while (true); var x = 1; ~~~~~~~~~~ !!! error TS7027: Unreachable code detected. module A { - ~~~~~~~~~~ while (true); - ~~~~~~~~~~~~~~~~~ let x; - ~~~~~~~~~~ ~~~~~~ !!! error TS7027: Unreachable code detected. } - ~ -!!! error TS7027: Unreachable code detected. module A1 { - ~~~~~~~~~~~ do {} while(true); - ~~~~~~~~~~~~~~~~~~~~~~ module A { - ~~~~~~~~~~~~~~ interface F {} - ~~~~~~~~~~~~~~~~~~~~~~ } - ~~~~~ } - ~ -!!! error TS7027: Unreachable code detected. module A2 { - ~~~~~~~~~~~ while (true); - ~~~~~~~~~~~~~~~~~ module A { - ~~~~~~~~~~~~~~ ~~~~~~~~~~ var x = 1; - ~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~ } ~~~~~ - ~~~~~ !!! error TS7027: Unreachable code detected. } - ~ -!!! error TS7027: Unreachable code detected. module A3 { - ~~~~~~~~~~~ while (true); - ~~~~~~~~~~~~~~~~~ type T = string; - ~~~~~~~~~~~~~~~~~~~~ } - ~ -!!! error TS7027: Unreachable code detected. module A4 { - ~~~~~~~~~~~ while (true); - ~~~~~~~~~~~~~~~~~ module A { - ~~~~~~~~~~~~~~ ~~~~~~~~~~ const enum E { X } - ~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~ } ~~~~~ - ~~~~~ !!! error TS7027: Unreachable code detected. } - ~ -!!! error TS7027: Unreachable code detected. function f1(x) { if (x) { @@ -111,16 +74,10 @@ reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. } module B { - ~~~~~~~~~~ for (; ;); - ~~~~~~~~~~~~~~ module C { - ~~~~~~~~~~~~~~ } - ~~~~~ } - ~ -!!! error TS7027: Unreachable code detected. function f3() { do { diff --git a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff deleted file mode 100644 index de5c1b581b..0000000000 --- a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff +++ /dev/null @@ -1,116 +0,0 @@ ---- old.reachabilityChecks1.errors.txt -+++ new.reachabilityChecks1.errors.txt -@@= skipped -0, +0 lines =@@ - reachabilityChecks1.ts(2,1): error TS7027: Unreachable code detected. -+reachabilityChecks1.ts(4,1): error TS7027: Unreachable code detected. - reachabilityChecks1.ts(6,5): error TS7027: Unreachable code detected. -+reachabilityChecks1.ts(9,1): error TS7027: Unreachable code detected. -+reachabilityChecks1.ts(16,1): error TS7027: Unreachable code detected. - reachabilityChecks1.ts(18,5): error TS7027: Unreachable code detected. -+reachabilityChecks1.ts(23,1): error TS7027: Unreachable code detected. -+reachabilityChecks1.ts(28,1): error TS7027: Unreachable code detected. - reachabilityChecks1.ts(30,5): error TS7027: Unreachable code detected. - reachabilityChecks1.ts(47,5): error TS7027: Unreachable code detected. -+reachabilityChecks1.ts(51,1): error TS7027: Unreachable code detected. - reachabilityChecks1.ts(60,5): error TS7027: Unreachable code detected. - reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. - - --==== reachabilityChecks1.ts (7 errors) ==== -+==== reachabilityChecks1.ts (13 errors) ==== - while (true); - var x = 1; - ~~~~~~~~~~ - !!! error TS7027: Unreachable code detected. - - module A { -+ ~~~~~~~~~~ - while (true); -+ ~~~~~~~~~~~~~~~~~ - let x; -+ ~~~~~~~~~~ - ~~~~~~ - !!! error TS7027: Unreachable code detected. - } -+ ~ -+!!! error TS7027: Unreachable code detected. - - module A1 { -+ ~~~~~~~~~~~ - do {} while(true); -+ ~~~~~~~~~~~~~~~~~~~~~~ - module A { -+ ~~~~~~~~~~~~~~ - interface F {} -+ ~~~~~~~~~~~~~~~~~~~~~~ - } -+ ~~~~~ - } -+ ~ -+!!! error TS7027: Unreachable code detected. - - module A2 { -+ ~~~~~~~~~~~ - while (true); -+ ~~~~~~~~~~~~~~~~~ - module A { -+ ~~~~~~~~~~~~~~ - ~~~~~~~~~~ - var x = 1; - ~~~~~~~~~~~~~~~~~~ -+ ~~~~~~~~~~~~~~~~~~ - } - ~~~~~ -+ ~~~~~ - !!! error TS7027: Unreachable code detected. - } -+ ~ -+!!! error TS7027: Unreachable code detected. - - module A3 { -+ ~~~~~~~~~~~ - while (true); -+ ~~~~~~~~~~~~~~~~~ - type T = string; -+ ~~~~~~~~~~~~~~~~~~~~ - } -+ ~ -+!!! error TS7027: Unreachable code detected. - - module A4 { -+ ~~~~~~~~~~~ - while (true); -+ ~~~~~~~~~~~~~~~~~ - module A { -+ ~~~~~~~~~~~~~~ - ~~~~~~~~~~ - const enum E { X } - ~~~~~~~~~~~~~~~~~~~~~~~~~~ -+ ~~~~~~~~~~~~~~~~~~~~~~~~~~ - } - ~~~~~ -+ ~~~~~ - !!! error TS7027: Unreachable code detected. - } -+ ~ -+!!! error TS7027: Unreachable code detected. - - function f1(x) { - if (x) { -@@= skipped -73, +110 lines =@@ - } - - module B { -+ ~~~~~~~~~~ - for (; ;); -+ ~~~~~~~~~~~~~~ - module C { -+ ~~~~~~~~~~~~~~ - } -+ ~~~~~ - } -+ ~ -+!!! error TS7027: Unreachable code detected. - - function f3() { - do { \ No newline at end of file From 2cd24d9941492ab3b36effb64bef464054cd739d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:54:28 -0800 Subject: [PATCH 04/18] Copilot feedback --- internal/checker/checker.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 07b67c2833..68616def1f 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2142,6 +2142,7 @@ func (c *Checker) checkSourceFile(ctx context.Context, sourceFile *ast.SourceFil } else { c.wasCanceled = true } + c.reportedUnreachableStatements.Clear() c.ctx = nil links.typeChecked = true } @@ -2357,7 +2358,7 @@ func (c *Checker) errorOnEachUnreachableRange(node *ast.Node, isError bool) { isPurelyTypeDeclaration := func(s *ast.Node) bool { switch s.Kind { - case ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration: + case ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration: return true case ast.KindModuleDeclaration: return ast.GetModuleInstanceState(s) != ast.ModuleInstanceStateInstantiated From b90223b47ec0def543d69a79e26a373caddfcfcc Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:56:55 -0800 Subject: [PATCH 05/18] Completely revamp implementation to use a NodeFlag, massively simplify, fix TS issue --- internal/ast/ast.go | 14 +- internal/ast/nodeflags.go | 1 + internal/ast/utilities.go | 6 + internal/binder/binder.go | 40 ++-- internal/checker/checker.go | 175 +++++------------- .../tstransforms/runtimesyntax.go | 2 +- .../transformers/tstransforms/typeeraser.go | 2 +- .../transformers/tstransforms/utilities.go | 6 - .../compiler/reachabilityChecks10.errors.txt | 11 ++ .../compiler/reachabilityChecks10.symbols | 16 ++ .../compiler/reachabilityChecks10.types | 22 +++ .../compiler/reachabilityChecks9.errors.txt | 37 ++++ .../compiler/reachabilityChecks9.symbols | 65 +++++++ .../compiler/reachabilityChecks9.types | 87 +++++++++ .../compiler/reachabilityChecks1.errors.txt | 50 +++-- .../reachabilityChecks1.errors.txt.diff | 118 ++++++++++++ .../cases/compiler/reachabilityChecks10.ts | 7 + .../cases/compiler/reachabilityChecks9.ts | 29 +++ 18 files changed, 529 insertions(+), 159 deletions(-) create mode 100644 testdata/baselines/reference/compiler/reachabilityChecks10.errors.txt create mode 100644 testdata/baselines/reference/compiler/reachabilityChecks10.symbols create mode 100644 testdata/baselines/reference/compiler/reachabilityChecks10.types create mode 100644 testdata/baselines/reference/compiler/reachabilityChecks9.errors.txt create mode 100644 testdata/baselines/reference/compiler/reachabilityChecks9.symbols create mode 100644 testdata/baselines/reference/compiler/reachabilityChecks9.types create mode 100644 testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff create mode 100644 testdata/tests/cases/compiler/reachabilityChecks10.ts create mode 100644 testdata/tests/cases/compiler/reachabilityChecks9.ts diff --git a/internal/ast/ast.go b/internal/ast/ast.go index cec4a4689c..abee405be0 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -631,6 +631,15 @@ func (n *Node) Statements() []*Node { return nil } +func (n *Node) CanHaveStatements() bool { + switch n.Kind { + case KindSourceFile, KindBlock, KindModuleBlock, KindCaseClause, KindDefaultClause: + return true + default: + return false + } +} + func (n *Node) ModifierFlags() ModifierFlags { modifiers := n.Modifiers() if modifiers != nil { @@ -3564,9 +3573,8 @@ func (node *DebuggerStatement) Clone(f NodeFactoryCoercible) *Node { type LabeledStatement struct { StatementBase - Label *IdentifierNode // IdentifierNode - Statement *Statement // Statement - IsReferenced bool // Set by binder to indicate if the label is used by a break or continue statement + Label *IdentifierNode // IdentifierNode + Statement *Statement // Statement } func (f *NodeFactory) NewLabeledStatement(label *IdentifierNode, statement *Statement) *Node { diff --git a/internal/ast/nodeflags.go b/internal/ast/nodeflags.go index 2feb4f2a52..d97a0f9344 100644 --- a/internal/ast/nodeflags.go +++ b/internal/ast/nodeflags.go @@ -42,6 +42,7 @@ const ( NodeFlagsInWithStatement NodeFlags = 1 << 24 // If any ancestor of node was the `statement` of a WithStatement (not the `expression`) NodeFlagsJsonFile NodeFlags = 1 << 25 // If node was parsed in a Json NodeFlagsDeprecated NodeFlags = 1 << 26 // If has '@deprecated' JSDoc tag + NodeFlagsUnreachable NodeFlags = 1 << 27 // If node is unreachable (set by binder, reported by checker) NodeFlagsBlockScoped = NodeFlagsLet | NodeFlagsConst | NodeFlagsUsing NodeFlagsConstant = NodeFlagsConst | NodeFlagsUsing diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index b381ac436d..70b39ff6a6 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -2336,6 +2336,12 @@ func getModuleInstanceStateForAliasTarget(node *Node, ancestors []*Node, visited return ModuleInstanceStateInstantiated } +func IsInstantiatedModule(node *Node, preserveConstEnums bool) bool { + moduleState := GetModuleInstanceState(node) + return moduleState == ModuleInstanceStateInstantiated || + (preserveConstEnums && moduleState == ModuleInstanceStateConstEnumOnly) +} + func NodeHasName(statement *Node, id *Node) bool { name := statement.Name() if name != nil { diff --git a/internal/binder/binder.go b/internal/binder/binder.go index 7387a3d292..bbae44fe0c 100644 --- a/internal/binder/binder.go +++ b/internal/binder/binder.go @@ -1534,22 +1534,24 @@ func (b *Binder) bindChildren(node *ast.Node) { // and set it before we descend into nodes that could actually be part of an assignment pattern. b.inAssignmentPattern = false - kind := node.Kind - // Set the flow node so the checker can determine reachability - // Set flow node data BEFORE checking unreachability, because we return early for unreachable nodes - // Set on: statements, class/enum/module declarations (but not function declarations which are hoisted) - if (ast.KindFirstStatement <= kind && kind <= ast.KindLastStatement) || ast.IsClassDeclaration(node) || ast.IsEnumDeclaration(node) || ast.IsModuleDeclaration(node) { - hasFlowNodeData := node.FlowNodeData() - if hasFlowNodeData != nil { - hasFlowNodeData.FlowNode = b.currentFlow + isPotentiallyExecutableStatement := ast.KindFirstStatement <= node.Kind && node.Kind <= ast.KindLastStatement || + ast.IsClassDeclaration(node) || ast.IsEnumDeclaration(node) || ast.IsModuleDeclaration(node) + + if isPotentiallyExecutableStatement { + if flowNodeData := node.FlowNodeData(); flowNodeData != nil { + flowNodeData.FlowNode = b.currentFlow } } - if b.currentFlow.Flags&ast.FlowFlagsUnreachable != 0 { + if b.currentFlow == b.unreachableFlow { + if isPotentiallyExecutableStatement { + node.Flags |= ast.NodeFlagsUnreachable + } b.bindEachChild(node) b.inAssignmentPattern = saveInAssignmentPattern return } + switch node.Kind { case ast.KindWhileStatement: b.bindWhileStatement(node) @@ -1660,6 +1662,20 @@ func (b *Binder) bindEachStatementFunctionsFirst(statements *ast.NodeList) { } } +func (b *Binder) isNotVarWithoutInitializer(node *ast.Node) bool { + if !ast.IsVariableStatement(node) { + return true + } + declarationList := node.AsVariableStatement().DeclarationList + if ast.GetCombinedNodeFlags(declarationList)&ast.NodeFlagsBlockScoped != 0 { + return true + } + declarations := declarationList.AsVariableDeclarationList().Declarations.Nodes + return core.Some(declarations, func(d *ast.Node) bool { + return d.Initializer() != nil + }) +} + func (b *Binder) setContinueTarget(node *ast.Node, target *ast.FlowLabel) *ast.FlowLabel { label := b.activeLabelList for label != nil && node.Parent.Kind == ast.KindLabeledStatement { @@ -2046,8 +2062,10 @@ func (b *Binder) bindLabeledStatement(node *ast.Node) { } b.bind(stmt.Label) b.bind(stmt.Statement) - // Store whether the label was referenced so the checker can report unused labels later - stmt.IsReferenced = b.activeLabelList.referenced + if !b.activeLabelList.referenced { + // Mark the label as unused; the checker will decide whether to report it + stmt.Label.Flags |= ast.NodeFlagsUnreachable + } b.activeLabelList = b.activeLabelList.next b.addAntecedent(postStatementLabel, b.currentFlow) b.currentFlow = b.finishFlowLabel(postStatementLabel) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index a9378e567e..03ca046a5f 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -774,7 +774,6 @@ type Checker struct { lastFlowNodeReachable bool flowNodeReachable map[*ast.FlowNode]bool flowNodePostSuper map[*ast.FlowNode]bool - reportedUnreachableStatements collections.Set[*ast.Node] renamedBindingElementsInTypes []*ast.Node contextualInfos []ContextualInfo inferenceContextInfos []InferenceContextInfo @@ -858,6 +857,8 @@ type Checker struct { activeTypeMappersCaches []map[string]*Type ambientModulesOnce sync.Once ambientModules []*ast.Symbol + withinUnreachableCode bool + reportedUnreachableNodes collections.Set[*ast.Node] } func NewChecker(program Program) *Checker { @@ -2142,8 +2143,8 @@ func (c *Checker) checkSourceFile(ctx context.Context, sourceFile *ast.SourceFil } else { c.wasCanceled = true } - c.reportedUnreachableStatements.Clear() c.ctx = nil + c.reportedUnreachableNodes.Clear() links.typeChecked = true } } @@ -2179,38 +2180,14 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) { } } } - // Check unreachable code - any node with FlowNodeData can be checked - flowNode := node.FlowNodeData() - if flowNode != nil && flowNode.FlowNode != nil && !c.isReachableFlowNode(flowNode.FlowNode) { - // report errors on all statements except empty ones - // report errors on class declarations - // report errors on enums with preserved emit - // report errors on instantiated modules - reportError := ast.IsStatementButNotDeclaration(node) && !ast.IsEmptyStatement(node) || - ast.IsClassDeclaration(node) || - isEnumDeclarationWithPreservedEmit(node, c.compilerOptions) || - ast.IsModuleDeclaration(node) && shouldReportErrorOnModuleDeclaration(node, c.compilerOptions) - if reportError && c.compilerOptions.AllowUnreachableCode != core.TSTrue { - // Only report if we haven't already reported this statement - if !c.reportedUnreachableStatements.Has(node) { - // unreachable code is reported if - // - user has explicitly asked about it AND - // - statement is in not ambient context (statements in ambient context is already an error - // so we should not report extras) AND - // - node is not variable statement OR - // - node is block scoped variable statement OR - // - node is not block scoped variable statement and at least one variable declaration has initializer - // Rationale: we don't want to report errors on non-initialized var's since they are hoisted - // On the other side we do want to report errors on non-initialized 'lets' because of TDZ - isError := c.compilerOptions.AllowUnreachableCode == core.TSFalse && node.Flags&ast.NodeFlagsAmbient == 0 && (!ast.IsVariableStatement(node) || - ast.GetCombinedNodeFlags(node.AsVariableStatement().DeclarationList)&ast.NodeFlagsBlockScoped != 0 || - core.Some(node.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool { - return d.Initializer() != nil - })) - c.errorOnEachUnreachableRange(node, isError) - } + + if !c.withinUnreachableCode { + if c.checkSourceElementUnreachable(node) { + c.withinUnreachableCode = true + defer func() { c.withinUnreachableCode = false }() } } + switch node.Kind { case ast.KindTypeParameter: c.checkTypeParameter(node) @@ -2333,112 +2310,58 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) { } } -func isEnumDeclarationWithPreservedEmit(node *ast.Node, options *core.CompilerOptions) bool { - return ast.IsEnumDeclaration(node) && (!ast.IsEnumConst(node) || options.ShouldPreserveConstEnums()) -} - -func shouldReportErrorOnModuleDeclaration(node *ast.Node, options *core.CompilerOptions) bool { - instanceState := ast.GetModuleInstanceState(node) - return instanceState == ast.ModuleInstanceStateInstantiated || (instanceState == ast.ModuleInstanceStateConstEnumOnly && options.ShouldPreserveConstEnums()) -} - -func (c *Checker) errorOnEachUnreachableRange(node *ast.Node, isError bool) { - errorOrSuggestion := func(first *ast.Node, last *ast.Node) { - sourceFile := ast.GetSourceFileOfNode(first) - textRange := core.NewTextRange(scanner.GetRangeOfTokenAtPosition(sourceFile, first.Pos()).Pos(), last.End()) - diagnostic := ast.NewDiagnostic(sourceFile, textRange, diagnostics.Unreachable_code_detected) - c.addErrorOrSuggestion(isError, diagnostic) +func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool { + if c.reportedUnreachableNodes.Has(node) { + return true } - markRangeAsReported := func(first *ast.Node, last *ast.Node, statements []*ast.Node) { - for _, markedNode := range statements[slices.Index(statements, first) : slices.Index(statements, last)+1] { - c.reportedUnreachableStatements.Add(markedNode) - } + if !c.isSourceElementUnreachable(node) { + return false } - isPurelyTypeDeclaration := func(s *ast.Node) bool { - switch s.Kind { - case ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration: - return true - case ast.KindModuleDeclaration: - return ast.GetModuleInstanceState(s) != ast.ModuleInstanceStateInstantiated - case ast.KindEnumDeclaration: - return ast.HasSyntacticModifier(s, ast.ModifierFlagsConst) && !c.compilerOptions.ShouldPreserveConstEnums() - default: - return false - } - } + c.reportedUnreachableNodes.Add(node) - isExecutableStatement := func(s *ast.Node) bool { - // Don't remove statements that can validly be used before they appear. - // This includes function declarations (which are hoisted), type declarations, and uninitialized vars. - return !isPurelyTypeDeclaration(s) && - !ast.IsFunctionDeclaration(s) && - !(ast.IsVariableStatement(s) && (ast.GetCombinedNodeFlags(s.AsVariableStatement().DeclarationList)&ast.NodeFlagsBlockScoped == 0) && - core.Every(s.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool { - return d.AsVariableDeclaration().Initializer == nil - })) - } + isError := c.compilerOptions.AllowUnreachableCode == core.TSFalse + sourceFile := ast.GetSourceFileOfNode(node) + + start := scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos()).Pos() + end := node.End() - scanAndReportExecutableStatements := func(statements []*ast.Node, index int) { - var first, last *ast.Node - for _, s := range statements[index:] { - if isExecutableStatement(s) { - if first == nil { - first = s + parent := node.Parent + if parent.CanHaveStatements() { + statements := parent.Statements() + if offset := slices.Index(statements, node); offset >= 0 { + for _, nextNode := range statements[offset+1:] { + if !c.isSourceElementUnreachable(nextNode) { + break } - last = s - } else { - break + end = nextNode.End() + c.reportedUnreachableNodes.Add(nextNode) } } - if first != nil { - errorOrSuggestion(first, last) - markRangeAsReported(first, last, statements) - } } - // Report unreachable code in blocks (function bodies, if/else blocks, etc.) - // and module blocks (the body of a module declaration). - // These scan forward to report ranges of consecutive unreachable statements. - if ast.IsBlock(node.Parent) && isExecutableStatement(node) { - scanAndReportExecutableStatements(node.Parent.AsBlock().Statements.Nodes, slices.Index(node.Parent.AsBlock().Statements.Nodes, node)) - return - } - if ast.IsModuleBlock(node.Parent) && isExecutableStatement(node) { - scanAndReportExecutableStatements(node.Parent.AsModuleBlock().Statements.Nodes, slices.Index(node.Parent.AsModuleBlock().Statements.Nodes, node)) - return - } + diagnostic := ast.NewDiagnostic(sourceFile, core.NewTextRange(start, end), diagnostics.Unreachable_code_detected) + c.addErrorOrSuggestion(isError, diagnostic) - // Top-level module declarations are never reported individually. - // Their contents are checked when the module body is visited. - // However, if there's no other unreachable code before them and no function declarations - // (which are hoisted), then report them to avoid silent unreachable modules. - if ast.IsModuleDeclaration(node) && ast.IsSourceFile(node.Parent) && shouldReportErrorOnModuleDeclaration(node, c.compilerOptions) { - if c.reportedUnreachableStatements.Has(node) { - return - } - statements := node.Parent.AsSourceFile().Statements.Nodes - index := slices.Index(statements, node) - // Don't report if there's a preceding function (hoisting makes flow complex) - // or other unreachable code (which would already be reported). - for i := range index { - s := statements[i] - if ast.IsFunctionDeclaration(s) { - return - } - if isExecutableStatement(s) && !ast.IsModuleDeclaration(s) { - if flowData := s.FlowNodeData(); flowData != nil && flowData.FlowNode != nil && !c.isReachableFlowNode(flowData.FlowNode) { - return - } - } + return true +} + +func (c *Checker) isSourceElementUnreachable(node *ast.Node) bool { + if node.Flags&ast.NodeFlagsUnreachable != 0 { + switch node.Kind { + case ast.KindEnumDeclaration: + return !ast.IsEnumConst(node) || c.compilerOptions.ShouldPreserveConstEnums() + case ast.KindModuleDeclaration: + return ast.IsInstantiatedModule(node, c.compilerOptions.ShouldPreserveConstEnums()) + default: + return true } - errorOrSuggestion(node, node) - return + } else if ast.KindFirstStatement <= node.Kind && node.Kind <= ast.KindLastStatement { + flowNode := node.FlowNodeData().FlowNode + return flowNode != nil && !c.isReachableFlowNode(flowNode) } - - // Default: report the node individually - errorOrSuggestion(node, node) + return false } // Function and class expression bodies are checked after all statements in the enclosing body. This is @@ -4155,8 +4078,8 @@ func (c *Checker) checkLabeledStatement(node *ast.Node) { } } } - // Check for unused labels - if !labeledStatement.IsReferenced && c.compilerOptions.AllowUnusedLabels != core.TSTrue { + // Check for unused label marked by the binder + if labelNode.Flags&ast.NodeFlagsUnreachable != 0 { c.errorOrSuggestion(c.compilerOptions.AllowUnusedLabels == core.TSFalse, labelNode, diagnostics.Unused_label) } c.checkSourceElement(labeledStatement.Statement) diff --git a/internal/transformers/tstransforms/runtimesyntax.go b/internal/transformers/tstransforms/runtimesyntax.go index d1fa5e5fb8..8c3ad6a817 100644 --- a/internal/transformers/tstransforms/runtimesyntax.go +++ b/internal/transformers/tstransforms/runtimesyntax.go @@ -1148,7 +1148,7 @@ func (tx *RuntimeSyntaxTransformer) shouldEmitModuleDeclaration(node *ast.Module // If we can't find a parse tree node, assume the node is instantiated. return true } - return isInstantiatedModule(node.AsNode(), tx.compilerOptions.ShouldPreserveConstEnums()) + return ast.IsInstantiatedModule(node.AsNode(), tx.compilerOptions.ShouldPreserveConstEnums()) } func getInnermostModuleDeclarationFromDottedModule(moduleDeclaration *ast.ModuleDeclaration) *ast.ModuleDeclaration { diff --git a/internal/transformers/tstransforms/typeeraser.go b/internal/transformers/tstransforms/typeeraser.go index 99c70a8f39..f17962f6d4 100644 --- a/internal/transformers/tstransforms/typeeraser.go +++ b/internal/transformers/tstransforms/typeeraser.go @@ -109,7 +109,7 @@ func (tx *TypeEraserTransformer) visit(node *ast.Node) *ast.Node { case ast.KindModuleDeclaration: if !ast.IsIdentifier(node.Name()) || - !isInstantiatedModule(node, tx.compilerOptions.ShouldPreserveConstEnums()) || + !ast.IsInstantiatedModule(node, tx.compilerOptions.ShouldPreserveConstEnums()) || getInnermostModuleDeclarationFromDottedModule(node.AsModuleDeclaration()).Body == nil { // TypeScript module declarations are elided if they are not instantiated or have no body return tx.elide(node) diff --git a/internal/transformers/tstransforms/utilities.go b/internal/transformers/tstransforms/utilities.go index ead7be4bb8..9e0f04f17f 100644 --- a/internal/transformers/tstransforms/utilities.go +++ b/internal/transformers/tstransforms/utilities.go @@ -33,9 +33,3 @@ func constantExpression(value any, factory *printer.NodeFactory) *ast.Expression } return nil } - -func isInstantiatedModule(node *ast.ModuleDeclarationNode, preserveConstEnums bool) bool { - moduleState := ast.GetModuleInstanceState(node) - return moduleState == ast.ModuleInstanceStateInstantiated || - (preserveConstEnums && moduleState == ast.ModuleInstanceStateConstEnumOnly) -} diff --git a/testdata/baselines/reference/compiler/reachabilityChecks10.errors.txt b/testdata/baselines/reference/compiler/reachabilityChecks10.errors.txt new file mode 100644 index 0000000000..2b26eae20f --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecks10.errors.txt @@ -0,0 +1,11 @@ +reachabilityChecks10.ts(2,1): error TS7027: Unreachable code detected. + + +==== reachabilityChecks10.ts (1 errors) ==== + throw new Error("") + console.log("1") + ~~~~~~~~~~~~~~~~ + console.log("2") + ~~~~~~~~~~~~~~~~ +!!! error TS7027: Unreachable code detected. + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/reachabilityChecks10.symbols b/testdata/baselines/reference/compiler/reachabilityChecks10.symbols new file mode 100644 index 0000000000..6ecc554153 --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecks10.symbols @@ -0,0 +1,16 @@ +//// [tests/cases/compiler/reachabilityChecks10.ts] //// + +=== reachabilityChecks10.ts === +throw new Error("") +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + +console.log("1") +>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, --, --)) + +console.log("2") +>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, --, --)) + diff --git a/testdata/baselines/reference/compiler/reachabilityChecks10.types b/testdata/baselines/reference/compiler/reachabilityChecks10.types new file mode 100644 index 0000000000..d57914a480 --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecks10.types @@ -0,0 +1,22 @@ +//// [tests/cases/compiler/reachabilityChecks10.ts] //// + +=== reachabilityChecks10.ts === +throw new Error("") +>new Error("") : Error +>Error : ErrorConstructor +>"" : "" + +console.log("1") +>console.log("1") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"1" : "1" + +console.log("2") +>console.log("2") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"2" : "2" + diff --git a/testdata/baselines/reference/compiler/reachabilityChecks9.errors.txt b/testdata/baselines/reference/compiler/reachabilityChecks9.errors.txt new file mode 100644 index 0000000000..75b9cf98da --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecks9.errors.txt @@ -0,0 +1,37 @@ +reachabilityChecks9.ts(7,7): error TS7027: Unreachable code detected. +reachabilityChecks9.ts(20,7): error TS7027: Unreachable code detected. + + +==== reachabilityChecks9.ts (2 errors) ==== + // https://github.com/microsoft/TypeScript/issues/55562 + + function g(str: string) { + switch (str) { + case "a": + return; + console.log("1"); + ~~~~~~~~~~~~~~~~~ + console.log("2"); + ~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS7027: Unreachable code detected. + case "b": + console.log("3"); + } + } + + function h(str: string) { + switch (str) { + case "a": + console.log("1"); + default: + return; + console.log("2"); + ~~~~~~~~~~~~~~~~~ + console.log("3"); + ~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS7027: Unreachable code detected. + case "b": + console.log("4"); + } + } + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/reachabilityChecks9.symbols b/testdata/baselines/reference/compiler/reachabilityChecks9.symbols new file mode 100644 index 0000000000..d50f997b9a --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecks9.symbols @@ -0,0 +1,65 @@ +//// [tests/cases/compiler/reachabilityChecks9.ts] //// + +=== reachabilityChecks9.ts === +// https://github.com/microsoft/TypeScript/issues/55562 + +function g(str: string) { +>g : Symbol(g, Decl(reachabilityChecks9.ts, 0, 0)) +>str : Symbol(str, Decl(reachabilityChecks9.ts, 2, 11)) + + switch (str) { +>str : Symbol(str, Decl(reachabilityChecks9.ts, 2, 11)) + + case "a": + return; + console.log("1"); +>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, --, --)) + + console.log("2"); +>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, --, --)) + + case "b": + console.log("3"); +>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, --, --)) + } +} + +function h(str: string) { +>h : Symbol(h, Decl(reachabilityChecks9.ts, 11, 1)) +>str : Symbol(str, Decl(reachabilityChecks9.ts, 13, 11)) + + switch (str) { +>str : Symbol(str, Decl(reachabilityChecks9.ts, 13, 11)) + + case "a": + console.log("1"); +>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, --, --)) + + default: + return; + console.log("2"); +>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, --, --)) + + console.log("3"); +>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, --, --)) + + case "b": + console.log("4"); +>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, --, --)) + } +} + diff --git a/testdata/baselines/reference/compiler/reachabilityChecks9.types b/testdata/baselines/reference/compiler/reachabilityChecks9.types new file mode 100644 index 0000000000..8bced2f44c --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecks9.types @@ -0,0 +1,87 @@ +//// [tests/cases/compiler/reachabilityChecks9.ts] //// + +=== reachabilityChecks9.ts === +// https://github.com/microsoft/TypeScript/issues/55562 + +function g(str: string) { +>g : (str: string) => void +>str : string + + switch (str) { +>str : string + + case "a": +>"a" : "a" + + return; + console.log("1"); +>console.log("1") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"1" : "1" + + console.log("2"); +>console.log("2") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"2" : "2" + + case "b": +>"b" : "b" + + console.log("3"); +>console.log("3") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"3" : "3" + } +} + +function h(str: string) { +>h : (str: string) => void +>str : string + + switch (str) { +>str : string + + case "a": +>"a" : "a" + + console.log("1"); +>console.log("1") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"1" : "1" + + default: + return; + console.log("2"); +>console.log("2") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"2" : "2" + + console.log("3"); +>console.log("3") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"3" : "3" + + case "b": +>"b" : "b" + + console.log("4"); +>console.log("4") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"4" : "4" + } +} + diff --git a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt index d25977a05b..d9f16a1e0d 100644 --- a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt +++ b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt @@ -1,58 +1,78 @@ reachabilityChecks1.ts(2,1): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(6,5): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(18,5): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(30,5): error TS7027: Unreachable code detected. +reachabilityChecks1.ts(42,5): error TS7027: Unreachable code detected. reachabilityChecks1.ts(47,5): error TS7027: Unreachable code detected. +reachabilityChecks1.ts(51,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(60,5): error TS7027: Unreachable code detected. reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. -==== reachabilityChecks1.ts (7 errors) ==== +==== reachabilityChecks1.ts (6 errors) ==== while (true); var x = 1; ~~~~~~~~~~ -!!! error TS7027: Unreachable code detected. + module A { + ~~~~~~~~~~ while (true); + ~~~~~~~~~~~~~~~~~ let x; - ~~~~~~ -!!! error TS7027: Unreachable code detected. + ~~~~~~~~~~ } + ~ + module A1 { + ~~~~~~~~~~~ do {} while(true); + ~~~~~~~~~~~~~~~~~~~~~~ module A { + ~~~~~~~~~~~~~~ interface F {} + ~~~~~~~~~~~~~~~~~~~~~~ } + ~~~~~ } + ~ + module A2 { + ~~~~~~~~~~~ while (true); + ~~~~~~~~~~~~~~~~~ module A { - ~~~~~~~~~~ + ~~~~~~~~~~~~~~ var x = 1; ~~~~~~~~~~~~~~~~~~ } ~~~~~ -!!! error TS7027: Unreachable code detected. } + ~ + module A3 { + ~~~~~~~~~~~ while (true); + ~~~~~~~~~~~~~~~~~ type T = string; + ~~~~~~~~~~~~~~~~~~~~ } + ~ + module A4 { + ~~~~~~~~~~~ while (true); + ~~~~~~~~~~~~~~~~~ module A { - ~~~~~~~~~~ + ~~~~~~~~~~~~~~ const enum E { X } ~~~~~~~~~~~~~~~~~~~~~~~~~~ } ~~~~~ -!!! error TS7027: Unreachable code detected. } + ~ +!!! error TS7027: Unreachable code detected. function f1(x) { if (x) { @@ -62,6 +82,8 @@ reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. throw new Error("123"); } var x; + ~~~~~~ +!!! error TS7027: Unreachable code detected. } function f2() { @@ -74,10 +96,16 @@ reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. } module B { + ~~~~~~~~~~ for (; ;); + ~~~~~~~~~~~~~~ module C { + ~~~~~~~~~~~~~~ } + ~~~~~ } + ~ +!!! error TS7027: Unreachable code detected. function f3() { do { diff --git a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff new file mode 100644 index 0000000000..b94213f691 --- /dev/null +++ b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff @@ -0,0 +1,118 @@ +--- old.reachabilityChecks1.errors.txt ++++ new.reachabilityChecks1.errors.txt +@@= skipped -0, +0 lines =@@ + reachabilityChecks1.ts(2,1): error TS7027: Unreachable code detected. +-reachabilityChecks1.ts(6,5): error TS7027: Unreachable code detected. +-reachabilityChecks1.ts(18,5): error TS7027: Unreachable code detected. +-reachabilityChecks1.ts(30,5): error TS7027: Unreachable code detected. ++reachabilityChecks1.ts(42,5): error TS7027: Unreachable code detected. + reachabilityChecks1.ts(47,5): error TS7027: Unreachable code detected. ++reachabilityChecks1.ts(51,1): error TS7027: Unreachable code detected. + reachabilityChecks1.ts(60,5): error TS7027: Unreachable code detected. + reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. + + +-==== reachabilityChecks1.ts (7 errors) ==== ++==== reachabilityChecks1.ts (6 errors) ==== + while (true); + var x = 1; + ~~~~~~~~~~ +-!!! error TS7027: Unreachable code detected. ++ + + module A { ++ ~~~~~~~~~~ + while (true); ++ ~~~~~~~~~~~~~~~~~ + let x; +- ~~~~~~ +-!!! error TS7027: Unreachable code detected. ++ ~~~~~~~~~~ + } ++ ~ ++ + + module A1 { ++ ~~~~~~~~~~~ + do {} while(true); ++ ~~~~~~~~~~~~~~~~~~~~~~ + module A { ++ ~~~~~~~~~~~~~~ + interface F {} ++ ~~~~~~~~~~~~~~~~~~~~~~ + } ++ ~~~~~ + } ++ ~ ++ + + module A2 { ++ ~~~~~~~~~~~ + while (true); ++ ~~~~~~~~~~~~~~~~~ + module A { +- ~~~~~~~~~~ ++ ~~~~~~~~~~~~~~ + var x = 1; + ~~~~~~~~~~~~~~~~~~ + } + ~~~~~ +-!!! error TS7027: Unreachable code detected. + } ++ ~ ++ + + module A3 { ++ ~~~~~~~~~~~ + while (true); ++ ~~~~~~~~~~~~~~~~~ + type T = string; ++ ~~~~~~~~~~~~~~~~~~~~ + } ++ ~ ++ + + module A4 { ++ ~~~~~~~~~~~ + while (true); ++ ~~~~~~~~~~~~~~~~~ + module A { +- ~~~~~~~~~~ ++ ~~~~~~~~~~~~~~ + const enum E { X } + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + } + ~~~~~ +-!!! error TS7027: Unreachable code detected. + } ++ ~ ++!!! error TS7027: Unreachable code detected. + + function f1(x) { + if (x) { +@@= skipped -61, +81 lines =@@ + throw new Error("123"); + } + var x; ++ ~~~~~~ ++!!! error TS7027: Unreachable code detected. + } + + function f2() { +@@= skipped -12, +14 lines =@@ + } + + module B { ++ ~~~~~~~~~~ + for (; ;); ++ ~~~~~~~~~~~~~~ + module C { ++ ~~~~~~~~~~~~~~ + } ++ ~~~~~ + } ++ ~ ++!!! error TS7027: Unreachable code detected. + + function f3() { + do { \ No newline at end of file diff --git a/testdata/tests/cases/compiler/reachabilityChecks10.ts b/testdata/tests/cases/compiler/reachabilityChecks10.ts new file mode 100644 index 0000000000..e4f17b935c --- /dev/null +++ b/testdata/tests/cases/compiler/reachabilityChecks10.ts @@ -0,0 +1,7 @@ +// @strict: true +// @noEmit: true +// @allowUnreachableCode: false + +throw new Error("") +console.log("1") +console.log("2") diff --git a/testdata/tests/cases/compiler/reachabilityChecks9.ts b/testdata/tests/cases/compiler/reachabilityChecks9.ts new file mode 100644 index 0000000000..601cdb65d4 --- /dev/null +++ b/testdata/tests/cases/compiler/reachabilityChecks9.ts @@ -0,0 +1,29 @@ +// @strict: true +// @noEmit: true +// @allowUnreachableCode: false + +// https://github.com/microsoft/TypeScript/issues/55562 + +function g(str: string) { + switch (str) { + case "a": + return; + console.log("1"); + console.log("2"); + case "b": + console.log("3"); + } +} + +function h(str: string) { + switch (str) { + case "a": + console.log("1"); + default: + return; + console.log("2"); + console.log("3"); + case "b": + console.log("4"); + } +} From a20f766ea33e8da8b4ff1a6e99bc1428d1c3ab87 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:00:07 -0800 Subject: [PATCH 06/18] Comment cleanup --- internal/ast/nodeflags.go | 2 +- internal/checker/checker.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/ast/nodeflags.go b/internal/ast/nodeflags.go index d97a0f9344..3261fc277c 100644 --- a/internal/ast/nodeflags.go +++ b/internal/ast/nodeflags.go @@ -42,7 +42,7 @@ const ( NodeFlagsInWithStatement NodeFlags = 1 << 24 // If any ancestor of node was the `statement` of a WithStatement (not the `expression`) NodeFlagsJsonFile NodeFlags = 1 << 25 // If node was parsed in a Json NodeFlagsDeprecated NodeFlags = 1 << 26 // If has '@deprecated' JSDoc tag - NodeFlagsUnreachable NodeFlags = 1 << 27 // If node is unreachable (set by binder, reported by checker) + NodeFlagsUnreachable NodeFlags = 1 << 27 // If node is unreachable according to the binder NodeFlagsBlockScoped = NodeFlagsLet | NodeFlagsConst | NodeFlagsUsing NodeFlagsConstant = NodeFlagsConst | NodeFlagsUsing diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 03ca046a5f..ecdb8f9ab6 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -4078,7 +4078,6 @@ func (c *Checker) checkLabeledStatement(node *ast.Node) { } } } - // Check for unused label marked by the binder if labelNode.Flags&ast.NodeFlagsUnreachable != 0 { c.errorOrSuggestion(c.compilerOptions.AllowUnusedLabels == core.TSFalse, labelNode, diagnostics.Unused_label) } From 6b72cb9cd6f583defd1c02372581cb6fab4d5c7f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:03:56 -0800 Subject: [PATCH 07/18] Delete leftover code --- internal/binder/binder.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/internal/binder/binder.go b/internal/binder/binder.go index bbae44fe0c..0364a01c37 100644 --- a/internal/binder/binder.go +++ b/internal/binder/binder.go @@ -1662,20 +1662,6 @@ func (b *Binder) bindEachStatementFunctionsFirst(statements *ast.NodeList) { } } -func (b *Binder) isNotVarWithoutInitializer(node *ast.Node) bool { - if !ast.IsVariableStatement(node) { - return true - } - declarationList := node.AsVariableStatement().DeclarationList - if ast.GetCombinedNodeFlags(declarationList)&ast.NodeFlagsBlockScoped != 0 { - return true - } - declarations := declarationList.AsVariableDeclarationList().Declarations.Nodes - return core.Some(declarations, func(d *ast.Node) bool { - return d.Initializer() != nil - }) -} - func (b *Binder) setContinueTarget(node *ast.Node, target *ast.FlowLabel) *ast.FlowLabel { label := b.activeLabelList for label != nil && node.Parent.Kind == ast.KindLabeledStatement { From b9c33cad759d4ac1916ab2b75367594195408b15 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:39:57 -0800 Subject: [PATCH 08/18] another test --- .../compiler/reachabilityChecks11.errors.txt | 107 ++++++++++++ .../compiler/reachabilityChecks11.js | 153 ++++++++++++++++++ .../compiler/reachabilityChecks11.symbols | 124 ++++++++++++++ .../compiler/reachabilityChecks11.types | 139 ++++++++++++++++ .../cases/compiler/reachabilityChecks11.ts | 76 +++++++++ 5 files changed, 599 insertions(+) create mode 100644 testdata/baselines/reference/compiler/reachabilityChecks11.errors.txt create mode 100644 testdata/baselines/reference/compiler/reachabilityChecks11.js create mode 100644 testdata/baselines/reference/compiler/reachabilityChecks11.symbols create mode 100644 testdata/baselines/reference/compiler/reachabilityChecks11.types create mode 100644 testdata/tests/cases/compiler/reachabilityChecks11.ts diff --git a/testdata/baselines/reference/compiler/reachabilityChecks11.errors.txt b/testdata/baselines/reference/compiler/reachabilityChecks11.errors.txt new file mode 100644 index 0000000000..9dbc67c906 --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecks11.errors.txt @@ -0,0 +1,107 @@ +reachabilityChecks11.ts(6,5): error TS7027: Unreachable code detected. +reachabilityChecks11.ts(18,5): error TS7027: Unreachable code detected. +reachabilityChecks11.ts(30,5): error TS7027: Unreachable code detected. +reachabilityChecks11.ts(42,5): error TS7027: Unreachable code detected. +reachabilityChecks11.ts(47,5): error TS7027: Unreachable code detected. +reachabilityChecks11.ts(60,5): error TS7027: Unreachable code detected. +reachabilityChecks11.ts(69,5): error TS7027: Unreachable code detected. + + +==== reachabilityChecks11.ts (7 errors) ==== + // while (true); + var x = 1; + + module A { + while (true); + let x; + ~~~~~~ +!!! error TS7027: Unreachable code detected. + } + + module A1 { + do {} while(true); + module A { + interface F {} + } + } + + module A2 { + while (true); + module A { + ~~~~~~~~~~ + var x = 1; + ~~~~~~~~~~~~~~~~~~ + } + ~~~~~ +!!! error TS7027: Unreachable code detected. + } + + module A3 { + while (true); + type T = string; + } + + module A4 { + while (true); + module A { + ~~~~~~~~~~ + const enum E { X } + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + } + ~~~~~ +!!! error TS7027: Unreachable code detected. + } + + function f1(x) { + if (x) { + return; + } + else { + throw new Error("123"); + } + var x; + ~~~~~~ +!!! error TS7027: Unreachable code detected. + } + + function f2() { + return; + class A { + ~~~~~~~~~ + } + ~~~~~ +!!! error TS7027: Unreachable code detected. + } + + module B { + for (; ;); + module C { + } + } + + function f3() { + do { + } while (true); + enum E { + ~~~~~~~~ + X = 1 + ~~~~~~~~~~~~~ + } + ~~~~~ +!!! error TS7027: Unreachable code detected. + } + + function f4() { + if (true) { + throw new Error(); + } + const enum E { + ~~~~~~~~~~~~~~ + X = 1 + ~~~~~~~~~~~~~ + } + ~~~~~ +!!! error TS7027: Unreachable code detected. + } + + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/reachabilityChecks11.js b/testdata/baselines/reference/compiler/reachabilityChecks11.js new file mode 100644 index 0000000000..1e69b95ff9 --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecks11.js @@ -0,0 +1,153 @@ +//// [tests/cases/compiler/reachabilityChecks11.ts] //// + +//// [reachabilityChecks11.ts] +// while (true); +var x = 1; + +module A { + while (true); + let x; +} + +module A1 { + do {} while(true); + module A { + interface F {} + } +} + +module A2 { + while (true); + module A { + var x = 1; + } +} + +module A3 { + while (true); + type T = string; +} + +module A4 { + while (true); + module A { + const enum E { X } + } +} + +function f1(x) { + if (x) { + return; + } + else { + throw new Error("123"); + } + var x; +} + +function f2() { + return; + class A { + } +} + +module B { + for (; ;); + module C { + } +} + +function f3() { + do { + } while (true); + enum E { + X = 1 + } +} + +function f4() { + if (true) { + throw new Error(); + } + const enum E { + X = 1 + } +} + + + +//// [reachabilityChecks11.js] +// while (true); +var x = 1; +var A; +(function (A) { + while (true) + ; + let x; +})(A || (A = {})); +var A1; +(function (A1) { + do { } while (true); +})(A1 || (A1 = {})); +var A2; +(function (A2) { + while (true) + ; + let A; + (function (A) { + var x = 1; + })(A || (A = {})); +})(A2 || (A2 = {})); +var A3; +(function (A3) { + while (true) + ; +})(A3 || (A3 = {})); +var A4; +(function (A4) { + while (true) + ; + let A; + (function (A) { + let E; + (function (E) { + E[E["X"] = 0] = "X"; + })(E || (E = {})); + })(A || (A = {})); +})(A4 || (A4 = {})); +function f1(x) { + if (x) { + return; + } + else { + throw new Error("123"); + } + var x; +} +function f2() { + return; + class A { + } +} +var B; +(function (B) { + for (;;) + ; +})(B || (B = {})); +function f3() { + do { + } while (true); + let E; + (function (E) { + E[E["X"] = 1] = "X"; + })(E || (E = {})); +} +function f4() { + if (true) { + throw new Error(); + } + let E; + (function (E) { + E[E["X"] = 1] = "X"; + })(E || (E = {})); +} diff --git a/testdata/baselines/reference/compiler/reachabilityChecks11.symbols b/testdata/baselines/reference/compiler/reachabilityChecks11.symbols new file mode 100644 index 0000000000..450ccad803 --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecks11.symbols @@ -0,0 +1,124 @@ +//// [tests/cases/compiler/reachabilityChecks11.ts] //// + +=== reachabilityChecks11.ts === +// while (true); +var x = 1; +>x : Symbol(x, Decl(reachabilityChecks11.ts, 1, 3)) + +module A { +>A : Symbol(A, Decl(reachabilityChecks11.ts, 1, 10)) + + while (true); + let x; +>x : Symbol(x, Decl(reachabilityChecks11.ts, 5, 7)) +} + +module A1 { +>A1 : Symbol(A1, Decl(reachabilityChecks11.ts, 6, 1)) + + do {} while(true); + module A { +>A : Symbol(A, Decl(reachabilityChecks11.ts, 9, 22)) + + interface F {} +>F : Symbol(F, Decl(reachabilityChecks11.ts, 10, 14)) + } +} + +module A2 { +>A2 : Symbol(A2, Decl(reachabilityChecks11.ts, 13, 1)) + + while (true); + module A { +>A : Symbol(A, Decl(reachabilityChecks11.ts, 16, 17)) + + var x = 1; +>x : Symbol(x, Decl(reachabilityChecks11.ts, 18, 11)) + } +} + +module A3 { +>A3 : Symbol(A3, Decl(reachabilityChecks11.ts, 20, 1)) + + while (true); + type T = string; +>T : Symbol(T, Decl(reachabilityChecks11.ts, 23, 17)) +} + +module A4 { +>A4 : Symbol(A4, Decl(reachabilityChecks11.ts, 25, 1)) + + while (true); + module A { +>A : Symbol(A, Decl(reachabilityChecks11.ts, 28, 17)) + + const enum E { X } +>E : Symbol(E, Decl(reachabilityChecks11.ts, 29, 14)) +>X : Symbol(E.X, Decl(reachabilityChecks11.ts, 30, 22)) + } +} + +function f1(x) { +>f1 : Symbol(f1, Decl(reachabilityChecks11.ts, 32, 1)) +>x : Symbol(x, Decl(reachabilityChecks11.ts, 34, 12), Decl(reachabilityChecks11.ts, 41, 7)) + + if (x) { +>x : Symbol(x, Decl(reachabilityChecks11.ts, 34, 12), Decl(reachabilityChecks11.ts, 41, 7)) + + return; + } + else { + throw new Error("123"); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } + var x; +>x : Symbol(x, Decl(reachabilityChecks11.ts, 34, 12), Decl(reachabilityChecks11.ts, 41, 7)) +} + +function f2() { +>f2 : Symbol(f2, Decl(reachabilityChecks11.ts, 42, 1)) + + return; + class A { +>A : Symbol(A, Decl(reachabilityChecks11.ts, 45, 11)) + } +} + +module B { +>B : Symbol(B, Decl(reachabilityChecks11.ts, 48, 1)) + + for (; ;); + module C { +>C : Symbol(C, Decl(reachabilityChecks11.ts, 51, 14)) + } +} + +function f3() { +>f3 : Symbol(f3, Decl(reachabilityChecks11.ts, 54, 1)) + + do { + } while (true); + enum E { +>E : Symbol(E, Decl(reachabilityChecks11.ts, 58, 19)) + + X = 1 +>X : Symbol(E.X, Decl(reachabilityChecks11.ts, 59, 12)) + } +} + +function f4() { +>f4 : Symbol(f4, Decl(reachabilityChecks11.ts, 62, 1)) + + if (true) { + throw new Error(); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } + const enum E { +>E : Symbol(E, Decl(reachabilityChecks11.ts, 67, 5)) + + X = 1 +>X : Symbol(E.X, Decl(reachabilityChecks11.ts, 68, 18)) + } +} + + diff --git a/testdata/baselines/reference/compiler/reachabilityChecks11.types b/testdata/baselines/reference/compiler/reachabilityChecks11.types new file mode 100644 index 0000000000..10cf27d433 --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecks11.types @@ -0,0 +1,139 @@ +//// [tests/cases/compiler/reachabilityChecks11.ts] //// + +=== reachabilityChecks11.ts === +// while (true); +var x = 1; +>x : number +>1 : 1 + +module A { +>A : typeof A + + while (true); +>true : true + + let x; +>x : any +} + +module A1 { +>A1 : typeof A1 + + do {} while(true); +>true : true + + module A { + interface F {} + } +} + +module A2 { +>A2 : typeof A2 + + while (true); +>true : true + + module A { +>A : typeof A + + var x = 1; +>x : number +>1 : 1 + } +} + +module A3 { +>A3 : typeof A3 + + while (true); +>true : true + + type T = string; +>T : string +} + +module A4 { +>A4 : typeof A4 + + while (true); +>true : true + + module A { + const enum E { X } +>E : E +>X : E.X + } +} + +function f1(x) { +>f1 : (x: any) => void +>x : any + + if (x) { +>x : any + + return; + } + else { + throw new Error("123"); +>new Error("123") : Error +>Error : ErrorConstructor +>"123" : "123" + } + var x; +>x : any +} + +function f2() { +>f2 : () => void + + return; + class A { +>A : A + } +} + +module B { +>B : typeof B + + for (; ;); + module C { + } +} + +function f3() { +>f3 : () => void + + do { + } while (true); +>true : true + + enum E { +>E : E + + X = 1 +>X : E.X +>1 : 1 + } +} + +function f4() { +>f4 : () => void + + if (true) { +>true : true + + throw new Error(); +>new Error() : Error +>Error : ErrorConstructor + } + const enum E { +>E : E + + X = 1 +>X : E.X +>1 : 1 + } +} + + diff --git a/testdata/tests/cases/compiler/reachabilityChecks11.ts b/testdata/tests/cases/compiler/reachabilityChecks11.ts new file mode 100644 index 0000000000..81dbcf3c7a --- /dev/null +++ b/testdata/tests/cases/compiler/reachabilityChecks11.ts @@ -0,0 +1,76 @@ +// @allowUnreachableCode: false +// @preserveConstEnums: true + +// while (true); +var x = 1; + +module A { + while (true); + let x; +} + +module A1 { + do {} while(true); + module A { + interface F {} + } +} + +module A2 { + while (true); + module A { + var x = 1; + } +} + +module A3 { + while (true); + type T = string; +} + +module A4 { + while (true); + module A { + const enum E { X } + } +} + +function f1(x) { + if (x) { + return; + } + else { + throw new Error("123"); + } + var x; +} + +function f2() { + return; + class A { + } +} + +module B { + for (; ;); + module C { + } +} + +function f3() { + do { + } while (true); + enum E { + X = 1 + } +} + +function f4() { + if (true) { + throw new Error(); + } + const enum E { + X = 1 + } +} + From 47412117a49a573a25c02c88619c7c99fc132948 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:58:04 -0800 Subject: [PATCH 09/18] Fix bugs caught by porting to Strada --- internal/checker/checker.go | 5 +++++ .../compiler/reachabilityChecks11.errors.txt | 5 +---- .../compiler/reachabilityChecks1.errors.txt | 5 +---- .../compiler/reachabilityChecks1.errors.txt.diff | 14 ++------------ 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index ecdb8f9ab6..f92c2ec2ac 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2354,6 +2354,11 @@ func (c *Checker) isSourceElementUnreachable(node *ast.Node) bool { return !ast.IsEnumConst(node) || c.compilerOptions.ShouldPreserveConstEnums() case ast.KindModuleDeclaration: return ast.IsInstantiatedModule(node, c.compilerOptions.ShouldPreserveConstEnums()) + case ast.KindVariableStatement: + declarationList := node.AsVariableStatement().DeclarationList + return ast.GetCombinedNodeFlags(declarationList)&ast.NodeFlagsBlockScoped != 0 || core.Some(declarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool { + return d.Initializer() != nil + }) default: return true } diff --git a/testdata/baselines/reference/compiler/reachabilityChecks11.errors.txt b/testdata/baselines/reference/compiler/reachabilityChecks11.errors.txt index 9dbc67c906..b4497ee464 100644 --- a/testdata/baselines/reference/compiler/reachabilityChecks11.errors.txt +++ b/testdata/baselines/reference/compiler/reachabilityChecks11.errors.txt @@ -1,13 +1,12 @@ reachabilityChecks11.ts(6,5): error TS7027: Unreachable code detected. reachabilityChecks11.ts(18,5): error TS7027: Unreachable code detected. reachabilityChecks11.ts(30,5): error TS7027: Unreachable code detected. -reachabilityChecks11.ts(42,5): error TS7027: Unreachable code detected. reachabilityChecks11.ts(47,5): error TS7027: Unreachable code detected. reachabilityChecks11.ts(60,5): error TS7027: Unreachable code detected. reachabilityChecks11.ts(69,5): error TS7027: Unreachable code detected. -==== reachabilityChecks11.ts (7 errors) ==== +==== reachabilityChecks11.ts (6 errors) ==== // while (true); var x = 1; @@ -60,8 +59,6 @@ reachabilityChecks11.ts(69,5): error TS7027: Unreachable code detected. throw new Error("123"); } var x; - ~~~~~~ -!!! error TS7027: Unreachable code detected. } function f2() { diff --git a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt index d9f16a1e0d..478dc40733 100644 --- a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt +++ b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt @@ -1,12 +1,11 @@ reachabilityChecks1.ts(2,1): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(42,5): error TS7027: Unreachable code detected. reachabilityChecks1.ts(47,5): error TS7027: Unreachable code detected. reachabilityChecks1.ts(51,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(60,5): error TS7027: Unreachable code detected. reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. -==== reachabilityChecks1.ts (6 errors) ==== +==== reachabilityChecks1.ts (5 errors) ==== while (true); var x = 1; ~~~~~~~~~~ @@ -82,8 +81,6 @@ reachabilityChecks1.ts(69,5): error TS7027: Unreachable code detected. throw new Error("123"); } var x; - ~~~~~~ -!!! error TS7027: Unreachable code detected. } function f2() { diff --git a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff index b94213f691..873affdebe 100644 --- a/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff +++ b/testdata/baselines/reference/submodule/compiler/reachabilityChecks1.errors.txt.diff @@ -5,7 +5,6 @@ -reachabilityChecks1.ts(6,5): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(18,5): error TS7027: Unreachable code detected. -reachabilityChecks1.ts(30,5): error TS7027: Unreachable code detected. -+reachabilityChecks1.ts(42,5): error TS7027: Unreachable code detected. reachabilityChecks1.ts(47,5): error TS7027: Unreachable code detected. +reachabilityChecks1.ts(51,1): error TS7027: Unreachable code detected. reachabilityChecks1.ts(60,5): error TS7027: Unreachable code detected. @@ -13,7 +12,7 @@ -==== reachabilityChecks1.ts (7 errors) ==== -+==== reachabilityChecks1.ts (6 errors) ==== ++==== reachabilityChecks1.ts (5 errors) ==== while (true); var x = 1; ~~~~~~~~~~ @@ -90,16 +89,7 @@ function f1(x) { if (x) { -@@= skipped -61, +81 lines =@@ - throw new Error("123"); - } - var x; -+ ~~~~~~ -+!!! error TS7027: Unreachable code detected. - } - - function f2() { -@@= skipped -12, +14 lines =@@ +@@= skipped -73, +92 lines =@@ } module B { From 173d31234d8e096ff186f551a656191d99823e76 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:05:28 -0800 Subject: [PATCH 10/18] Closer to what Strada would do --- internal/checker/checker.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index f92c2ec2ac..a9bb6ae9ac 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2161,10 +2161,12 @@ func (c *Checker) checkSourceElements(nodes []*ast.Node) { func (c *Checker) checkSourceElement(node *ast.Node) bool { if node != nil { saveCurrentNode := c.currentNode + saveWithinUnreachableCode := c.withinUnreachableCode c.currentNode = node c.instantiationCount = 0 c.checkSourceElementWorker(node) c.currentNode = saveCurrentNode + c.withinUnreachableCode = saveWithinUnreachableCode } return false } @@ -2184,7 +2186,6 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) { if !c.withinUnreachableCode { if c.checkSourceElementUnreachable(node) { c.withinUnreachableCode = true - defer func() { c.withinUnreachableCode = false }() } } From 8eb8faf6c57e65a31c9f75cf59c06cc94f30a8c9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:01:57 -0800 Subject: [PATCH 11/18] Even more simplfication --- internal/ast/utilities.go | 18 ++++++++++++++++++ internal/binder/binder.go | 17 +++++++---------- internal/checker/checker.go | 17 ++++++++--------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 70b39ff6a6..72ba07ee60 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -3838,3 +3838,21 @@ func GetFirstConstructorWithBody(node *Node) *Node { } return nil } + +// Returns true for nodes that are considered executable for the purposes of unreachable code detection. +func IsPotentiallyExecutableNode(node *Node) bool { + if KindFirstStatement <= node.Kind && node.Kind <= KindLastStatement { + if IsVariableStatement(node) { + declarationList := node.AsVariableStatement().DeclarationList + if GetCombinedNodeFlags(declarationList)&NodeFlagsBlockScoped != 0 { + return true + } + declarations := declarationList.AsVariableDeclarationList().Declarations.Nodes + return core.Some(declarations, func(d *Node) bool { + return d.Initializer() != nil + }) + } + return true + } + return IsClassDeclaration(node) || IsEnumDeclaration(node) || IsModuleDeclaration(node) +} diff --git a/internal/binder/binder.go b/internal/binder/binder.go index 0364a01c37..2ee90bcef4 100644 --- a/internal/binder/binder.go +++ b/internal/binder/binder.go @@ -1534,17 +1534,8 @@ func (b *Binder) bindChildren(node *ast.Node) { // and set it before we descend into nodes that could actually be part of an assignment pattern. b.inAssignmentPattern = false - isPotentiallyExecutableStatement := ast.KindFirstStatement <= node.Kind && node.Kind <= ast.KindLastStatement || - ast.IsClassDeclaration(node) || ast.IsEnumDeclaration(node) || ast.IsModuleDeclaration(node) - - if isPotentiallyExecutableStatement { - if flowNodeData := node.FlowNodeData(); flowNodeData != nil { - flowNodeData.FlowNode = b.currentFlow - } - } - if b.currentFlow == b.unreachableFlow { - if isPotentiallyExecutableStatement { + if ast.IsPotentiallyExecutableNode(node) { node.Flags |= ast.NodeFlagsUnreachable } b.bindEachChild(node) @@ -1552,6 +1543,12 @@ func (b *Binder) bindChildren(node *ast.Node) { return } + if ast.KindFirstStatement <= node.Kind && node.Kind <= ast.KindLastStatement { + if flowNodeData := node.FlowNodeData(); flowNodeData != nil { + flowNodeData.FlowNode = b.currentFlow + } + } + switch node.Kind { case ast.KindWhileStatement: b.bindWhileStatement(node) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index a9bb6ae9ac..011e24f06e 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2312,6 +2312,10 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) { } func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool { + if !ast.IsPotentiallyExecutableNode(node) { + return false + } + if c.reportedUnreachableNodes.Has(node) { return true } @@ -2333,7 +2337,7 @@ func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool { statements := parent.Statements() if offset := slices.Index(statements, node); offset >= 0 { for _, nextNode := range statements[offset+1:] { - if !c.isSourceElementUnreachable(nextNode) { + if !ast.IsPotentiallyExecutableNode(nextNode) || !c.isSourceElementUnreachable(nextNode) { break } end = nextNode.End() @@ -2349,23 +2353,18 @@ func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool { } func (c *Checker) isSourceElementUnreachable(node *ast.Node) bool { + // Precondition: ast.IsPotentiallyExecutableNode is true if node.Flags&ast.NodeFlagsUnreachable != 0 { switch node.Kind { case ast.KindEnumDeclaration: return !ast.IsEnumConst(node) || c.compilerOptions.ShouldPreserveConstEnums() case ast.KindModuleDeclaration: return ast.IsInstantiatedModule(node, c.compilerOptions.ShouldPreserveConstEnums()) - case ast.KindVariableStatement: - declarationList := node.AsVariableStatement().DeclarationList - return ast.GetCombinedNodeFlags(declarationList)&ast.NodeFlagsBlockScoped != 0 || core.Some(declarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool { - return d.Initializer() != nil - }) default: return true } - } else if ast.KindFirstStatement <= node.Kind && node.Kind <= ast.KindLastStatement { - flowNode := node.FlowNodeData().FlowNode - return flowNode != nil && !c.isReachableFlowNode(flowNode) + } else if flowNode := node.FlowNodeData().FlowNode; flowNode != nil { + return !c.isReachableFlowNode(flowNode) } return false } From 7081d00a3dd6d8b95c2ff73b0cc69643530d19f0 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:54:33 -0800 Subject: [PATCH 12/18] Add test that failed in strada --- .../compiler/reachabilityChecksIgnored.js | 29 +++++++++++++++ .../reachabilityChecksIgnored.symbols | 29 +++++++++++++++ .../compiler/reachabilityChecksIgnored.types | 37 +++++++++++++++++++ .../compiler/reachabilityChecksIgnored.ts | 17 +++++++++ 4 files changed, 112 insertions(+) create mode 100644 testdata/baselines/reference/compiler/reachabilityChecksIgnored.js create mode 100644 testdata/baselines/reference/compiler/reachabilityChecksIgnored.symbols create mode 100644 testdata/baselines/reference/compiler/reachabilityChecksIgnored.types create mode 100644 testdata/tests/cases/compiler/reachabilityChecksIgnored.ts diff --git a/testdata/baselines/reference/compiler/reachabilityChecksIgnored.js b/testdata/baselines/reference/compiler/reachabilityChecksIgnored.js new file mode 100644 index 0000000000..7635105d49 --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecksIgnored.js @@ -0,0 +1,29 @@ +//// [tests/cases/compiler/reachabilityChecksIgnored.ts] //// + +//// [reachabilityChecksIgnored.ts] +function a() { + throw new Error(""); + + // @ts-ignore + console.log("unreachable"); +} + +function b() { + throw new Error(""); + + // @ts-expect-error + console.log("unreachable"); +} + + +//// [reachabilityChecksIgnored.js] +function a() { + throw new Error(""); + // @ts-ignore + console.log("unreachable"); +} +function b() { + throw new Error(""); + // @ts-expect-error + console.log("unreachable"); +} diff --git a/testdata/baselines/reference/compiler/reachabilityChecksIgnored.symbols b/testdata/baselines/reference/compiler/reachabilityChecksIgnored.symbols new file mode 100644 index 0000000000..04eccd0375 --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecksIgnored.symbols @@ -0,0 +1,29 @@ +//// [tests/cases/compiler/reachabilityChecksIgnored.ts] //// + +=== reachabilityChecksIgnored.ts === +function a() { +>a : Symbol(a, Decl(reachabilityChecksIgnored.ts, 0, 0)) + + throw new Error(""); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + + // @ts-ignore + console.log("unreachable"); +>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, --, --)) +} + +function b() { +>b : Symbol(b, Decl(reachabilityChecksIgnored.ts, 5, 1)) + + throw new Error(""); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + + // @ts-expect-error + console.log("unreachable"); +>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, --, --)) +} + diff --git a/testdata/baselines/reference/compiler/reachabilityChecksIgnored.types b/testdata/baselines/reference/compiler/reachabilityChecksIgnored.types new file mode 100644 index 0000000000..838060e048 --- /dev/null +++ b/testdata/baselines/reference/compiler/reachabilityChecksIgnored.types @@ -0,0 +1,37 @@ +//// [tests/cases/compiler/reachabilityChecksIgnored.ts] //// + +=== reachabilityChecksIgnored.ts === +function a() { +>a : () => void + + throw new Error(""); +>new Error("") : Error +>Error : ErrorConstructor +>"" : "" + + // @ts-ignore + console.log("unreachable"); +>console.log("unreachable") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"unreachable" : "unreachable" +} + +function b() { +>b : () => void + + throw new Error(""); +>new Error("") : Error +>Error : ErrorConstructor +>"" : "" + + // @ts-expect-error + console.log("unreachable"); +>console.log("unreachable") : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>"unreachable" : "unreachable" +} + diff --git a/testdata/tests/cases/compiler/reachabilityChecksIgnored.ts b/testdata/tests/cases/compiler/reachabilityChecksIgnored.ts new file mode 100644 index 0000000000..c4f0145eef --- /dev/null +++ b/testdata/tests/cases/compiler/reachabilityChecksIgnored.ts @@ -0,0 +1,17 @@ +// @allowUnreachableCode: false +// @preserveConstEnums: true + + +function a() { + throw new Error(""); + + // @ts-ignore + console.log("unreachable"); +} + +function b() { + throw new Error(""); + + // @ts-expect-error + console.log("unreachable"); +} From ed17c21ab45871f371cb3155ef4c4b55946e1f11 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:18:57 -0800 Subject: [PATCH 13/18] Fix tristate --- internal/checker/checker.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 011e24f06e..f073c9a832 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2183,7 +2183,7 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) { } } - if !c.withinUnreachableCode { + if !c.withinUnreachableCode && c.compilerOptions.AllowUnreachableCode != core.TSTrue { if c.checkSourceElementUnreachable(node) { c.withinUnreachableCode = true } @@ -2326,7 +2326,6 @@ func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool { c.reportedUnreachableNodes.Add(node) - isError := c.compilerOptions.AllowUnreachableCode == core.TSFalse sourceFile := ast.GetSourceFileOfNode(node) start := scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos()).Pos() @@ -2347,7 +2346,7 @@ func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool { } diagnostic := ast.NewDiagnostic(sourceFile, core.NewTextRange(start, end), diagnostics.Unreachable_code_detected) - c.addErrorOrSuggestion(isError, diagnostic) + c.addErrorOrSuggestion(c.compilerOptions.AllowUnreachableCode == core.TSFalse, diagnostic) return true } @@ -4083,7 +4082,7 @@ func (c *Checker) checkLabeledStatement(node *ast.Node) { } } } - if labelNode.Flags&ast.NodeFlagsUnreachable != 0 { + if labelNode.Flags&ast.NodeFlagsUnreachable != 0 && c.compilerOptions.AllowUnusedLabels != core.TSTrue { c.errorOrSuggestion(c.compilerOptions.AllowUnusedLabels == core.TSFalse, labelNode, diagnostics.Unused_label) } c.checkSourceElement(labeledStatement.Statement) From a5292d557e0a81d42d5a79cc1d1a7fd22578a6f8 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:14:22 -0800 Subject: [PATCH 14/18] Add TODO about range diagnostics --- internal/checker/checker.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index f073c9a832..973a4cc620 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2331,6 +2331,9 @@ func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool { start := scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos()).Pos() end := node.End() + // TODO: if we are doing range diagnostics, it's possible we _haven't_ + // reported nodes before us in the same statement list. We should walk backwards + // and extend the range to include any prior unreachable unreported nodes as well. parent := node.Parent if parent.CanHaveStatements() { statements := parent.Statements() From ca4f60310b085d23563e6d94eae1c1e39f0059c1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:59:55 -0800 Subject: [PATCH 15/18] Add code for the future to not break region diags --- internal/checker/checker.go | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 973a4cc620..4713f7adb2 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2328,26 +2328,44 @@ func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool { sourceFile := ast.GetSourceFileOfNode(node) - start := scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos()).Pos() + start := node.Pos() end := node.End() - // TODO: if we are doing range diagnostics, it's possible we _haven't_ - // reported nodes before us in the same statement list. We should walk backwards - // and extend the range to include any prior unreachable unreported nodes as well. parent := node.Parent if parent.CanHaveStatements() { statements := parent.Statements() if offset := slices.Index(statements, node); offset >= 0 { - for _, nextNode := range statements[offset+1:] { + // Scan backwards to find the first unreachable unreported node; + // this may happen when producing region diagnostics where not all nodes + // will have been visited. + // TODO: enable this code once we support region diagnostics again. + first := offset + // for i := offset - 1; i >= 0; i-- { + // prevNode := statements[i] + // if !ast.IsPotentiallyExecutableNode(prevNode) || c.reportedUnreachableNodes.Has(prevNode) || !c.isSourceElementUnreachable(prevNode) { + // break + // } + // firstUnreachableIndex = i + // c.reportedUnreachableNodes.Add(prevNode) + // } + + last := offset + for i := offset + 1; i < len(statements); i++ { + nextNode := statements[i] if !ast.IsPotentiallyExecutableNode(nextNode) || !c.isSourceElementUnreachable(nextNode) { break } - end = nextNode.End() + last = i c.reportedUnreachableNodes.Add(nextNode) } + + start = statements[first].Pos() + end = statements[last].End() } } + start = scanner.GetRangeOfTokenAtPosition(sourceFile, start).Pos() + diagnostic := ast.NewDiagnostic(sourceFile, core.NewTextRange(start, end), diagnostics.Unreachable_code_detected) c.addErrorOrSuggestion(c.compilerOptions.AllowUnreachableCode == core.TSFalse, diagnostic) From 0b2e84aeea213dc0ae95550d932d51edc674e1fd Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:48:35 -0800 Subject: [PATCH 16/18] Set flow to nil on unreachable --- internal/binder/binder.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/binder/binder.go b/internal/binder/binder.go index 2ee90bcef4..e2b41a7c48 100644 --- a/internal/binder/binder.go +++ b/internal/binder/binder.go @@ -1535,6 +1535,9 @@ func (b *Binder) bindChildren(node *ast.Node) { b.inAssignmentPattern = false if b.currentFlow == b.unreachableFlow { + if flowNodeData := node.FlowNodeData(); flowNodeData != nil { + flowNodeData.FlowNode = nil + } if ast.IsPotentiallyExecutableNode(node) { node.Flags |= ast.NodeFlagsUnreachable } From 8cd169b5b22192b4c4fbb961cb391c3d0ac60691 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:36:15 -0800 Subject: [PATCH 17/18] comments --- internal/checker/checker.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 0bc006974b..9e710d70c1 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2377,6 +2377,8 @@ func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool { func (c *Checker) isSourceElementUnreachable(node *ast.Node) bool { // Precondition: ast.IsPotentiallyExecutableNode is true if node.Flags&ast.NodeFlagsUnreachable != 0 { + // The binder has determined that this code is unreachable. + // Ignore const enums unless preserveConstEnums is set. switch node.Kind { case ast.KindEnumDeclaration: return !ast.IsEnumConst(node) || c.compilerOptions.ShouldPreserveConstEnums() @@ -2386,6 +2388,7 @@ func (c *Checker) isSourceElementUnreachable(node *ast.Node) bool { return true } } else if flowNode := node.FlowNodeData().FlowNode; flowNode != nil { + // For code the binder doesn't know is unreachable, use control flow / types. return !c.isReachableFlowNode(flowNode) } return false From 7c0a60e0e554444cedfa8ca36ca0f75390df81ab Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:44:11 -0800 Subject: [PATCH 18/18] Just skip trivia, thank you daniel --- internal/checker/checker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 9e710d70c1..4fcf30c598 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2366,7 +2366,7 @@ func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool { } } - start = scanner.GetRangeOfTokenAtPosition(sourceFile, start).Pos() + start = scanner.SkipTrivia(sourceFile.Text(), start) diagnostic := ast.NewDiagnostic(sourceFile, core.NewTextRange(start, end), diagnostics.Unreachable_code_detected) c.addErrorOrSuggestion(c.compilerOptions.AllowUnreachableCode == core.TSFalse, diagnostic)