Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
6 changes: 6 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
136 changes: 21 additions & 115 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) {

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

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
77 changes: 72 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]
}

func NewChecker(program Program) *Checker {
Expand Down Expand Up @@ -2142,6 +2144,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 @@ -2158,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
}
Expand All @@ -2177,13 +2182,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 {
if c.checkSourceElementUnreachable(node) {
c.withinUnreachableCode = true
}
}

switch node.Kind {
case ast.KindTypeParameter:
c.checkTypeParameter(node)
Expand Down Expand Up @@ -2306,6 +2311,65 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) {
}
}

func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool {
if c.reportedUnreachableNodes.Has(node) {
return true
}

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

c.reportedUnreachableNodes.Add(node)

isError := c.compilerOptions.AllowUnreachableCode == core.TSFalse
sourceFile := ast.GetSourceFileOfNode(node)

start := scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos()).Pos()
end := node.End()

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
}
end = nextNode.End()
c.reportedUnreachableNodes.Add(nextNode)
}
}
}

diagnostic := ast.NewDiagnostic(sourceFile, core.NewTextRange(start, end), diagnostics.Unreachable_code_detected)
c.addErrorOrSuggestion(isError, diagnostic)

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())
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)
}
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 @@ -4020,6 +4084,9 @@ func (c *Checker) checkLabeledStatement(node *ast.Node) {
}
}
}
if labelNode.Flags&ast.NodeFlagsUnreachable != 0 {
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
2 changes: 1 addition & 1 deletion internal/transformers/tstransforms/runtimesyntax.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/transformers/tstransforms/typeeraser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading