Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,15 @@ func (n *Node) Statements() []*Node {
return nil
}

func (n *Node) CanHaveStatements() bool {
Copy link
Member Author

Choose a reason for hiding this comment

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

Alternatively, Statements() could return nil. I am unsure if that's a great idea.

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 {
Expand Down
1 change: 1 addition & 0 deletions internal/ast/nodeflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 according to the binder

NodeFlagsBlockScoped = NodeFlagsLet | NodeFlagsConst | NodeFlagsUsing
NodeFlagsConstant = NodeFlagsConst | NodeFlagsUsing
Expand Down
24 changes: 24 additions & 0 deletions internal/ast/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -3832,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)
}
132 changes: 19 additions & 113 deletions internal/binder/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1535,18 +1533,25 @@ 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) {

if b.currentFlow == b.unreachableFlow {
if flowNodeData := node.FlowNodeData(); flowNodeData != nil {
flowNodeData.FlowNode = nil
}
if ast.IsPotentiallyExecutableNode(node) {
node.Flags |= ast.NodeFlagsUnreachable
}
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) {
hasFlowNodeData := node.FlowNodeData()
if hasFlowNodeData != nil {
hasFlowNodeData.FlowNode = b.currentFlow

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)
Expand Down Expand Up @@ -1657,94 +1662,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 {
Expand Down Expand Up @@ -2131,8 +2048,9 @@ 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)
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)
Expand Down Expand Up @@ -2454,10 +2372,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 {
Expand Down Expand Up @@ -2749,14 +2663,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:
Expand Down
99 changes: 94 additions & 5 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,8 @@ type Checker struct {
activeTypeMappersCaches []map[string]*Type
ambientModulesOnce sync.Once
ambientModules []*ast.Symbol
withinUnreachableCode bool
reportedUnreachableNodes collections.Set[*ast.Node]

mu sync.Mutex
}
Expand Down Expand Up @@ -2144,6 +2146,7 @@ func (c *Checker) checkSourceFile(ctx context.Context, sourceFile *ast.SourceFil
c.wasCanceled = true
}
c.ctx = nil
c.reportedUnreachableNodes.Clear()
links.typeChecked = true
}
}
Expand All @@ -2160,10 +2163,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
}
Expand All @@ -2179,13 +2184,13 @@ 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)

if !c.withinUnreachableCode && c.compilerOptions.AllowUnreachableCode != core.TSTrue {
if c.checkSourceElementUnreachable(node) {
c.withinUnreachableCode = true
}
}

switch node.Kind {
case ast.KindTypeParameter:
c.checkTypeParameter(node)
Expand Down Expand Up @@ -2308,6 +2313,87 @@ 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
}

if !c.isSourceElementUnreachable(node) {
return false
}

c.reportedUnreachableNodes.Add(node)

sourceFile := ast.GetSourceFileOfNode(node)

start := node.Pos()
end := node.End()

parent := node.Parent
if parent.CanHaveStatements() {
statements := parent.Statements()
if offset := slices.Index(statements, node); offset >= 0 {
// 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
}
last = i
c.reportedUnreachableNodes.Add(nextNode)
}

start = statements[first].Pos()
end = statements[last].End()
}
}

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)

return true
}

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()
case ast.KindModuleDeclaration:
return ast.IsInstantiatedModule(node, c.compilerOptions.ShouldPreserveConstEnums())
default:
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
}

// Function and class expression bodies are checked after all statements in the enclosing body. This is
// to ensure constructs like the following are permitted:
//
Expand Down Expand Up @@ -4022,6 +4108,9 @@ func (c *Checker) checkLabeledStatement(node *ast.Node) {
}
}
}
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)
}

Expand Down
Loading