Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 3 additions & 2 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
130 changes: 16 additions & 114 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,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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
148 changes: 143 additions & 5 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2177,11 +2178,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 {
// 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 {
Expand Down Expand Up @@ -2306,6 +2332,114 @@ 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)
}

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
}))
}

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
}
last = s
} else {
break
}
}
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
}

// 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
// to ensure constructs like the following are permitted:
//
Expand Down Expand Up @@ -4020,6 +4154,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)
}

Expand Down
10 changes: 2 additions & 8 deletions internal/core/compileroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading