Skip to content

Commit a99b7f2

Browse files
authored
Move unreachable checks to checker, allowing more AST reuse (#2067)
1 parent bbde564 commit a99b7f2

30 files changed

+1312
-150
lines changed

internal/ast/ast.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,15 @@ func (n *Node) Statements() []*Node {
631631
return nil
632632
}
633633

634+
func (n *Node) CanHaveStatements() bool {
635+
switch n.Kind {
636+
case KindSourceFile, KindBlock, KindModuleBlock, KindCaseClause, KindDefaultClause:
637+
return true
638+
default:
639+
return false
640+
}
641+
}
642+
634643
func (n *Node) ModifierFlags() ModifierFlags {
635644
modifiers := n.Modifiers()
636645
if modifiers != nil {

internal/ast/nodeflags.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const (
4242
NodeFlagsInWithStatement NodeFlags = 1 << 24 // If any ancestor of node was the `statement` of a WithStatement (not the `expression`)
4343
NodeFlagsJsonFile NodeFlags = 1 << 25 // If node was parsed in a Json
4444
NodeFlagsDeprecated NodeFlags = 1 << 26 // If has '@deprecated' JSDoc tag
45+
NodeFlagsUnreachable NodeFlags = 1 << 27 // If node is unreachable according to the binder
4546

4647
NodeFlagsBlockScoped = NodeFlagsLet | NodeFlagsConst | NodeFlagsUsing
4748
NodeFlagsConstant = NodeFlagsConst | NodeFlagsUsing

internal/ast/utilities.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2336,6 +2336,12 @@ func getModuleInstanceStateForAliasTarget(node *Node, ancestors []*Node, visited
23362336
return ModuleInstanceStateInstantiated
23372337
}
23382338

2339+
func IsInstantiatedModule(node *Node, preserveConstEnums bool) bool {
2340+
moduleState := GetModuleInstanceState(node)
2341+
return moduleState == ModuleInstanceStateInstantiated ||
2342+
(preserveConstEnums && moduleState == ModuleInstanceStateConstEnumOnly)
2343+
}
2344+
23392345
func NodeHasName(statement *Node, id *Node) bool {
23402346
name := statement.Name()
23412347
if name != nil {
@@ -3832,3 +3838,21 @@ func GetFirstConstructorWithBody(node *Node) *Node {
38323838
}
38333839
return nil
38343840
}
3841+
3842+
// Returns true for nodes that are considered executable for the purposes of unreachable code detection.
3843+
func IsPotentiallyExecutableNode(node *Node) bool {
3844+
if KindFirstStatement <= node.Kind && node.Kind <= KindLastStatement {
3845+
if IsVariableStatement(node) {
3846+
declarationList := node.AsVariableStatement().DeclarationList
3847+
if GetCombinedNodeFlags(declarationList)&NodeFlagsBlockScoped != 0 {
3848+
return true
3849+
}
3850+
declarations := declarationList.AsVariableDeclarationList().Declarations.Nodes
3851+
return core.Some(declarations, func(d *Node) bool {
3852+
return d.Initializer() != nil
3853+
})
3854+
}
3855+
return true
3856+
}
3857+
return IsClassDeclaration(node) || IsEnumDeclaration(node) || IsModuleDeclaration(node)
3858+
}

internal/binder/binder.go

Lines changed: 19 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,9 @@ const (
4242
)
4343

4444
type Binder struct {
45-
file *ast.SourceFile
46-
bindFunc func(*ast.Node) bool
47-
unreachableFlow *ast.FlowNode
48-
reportedUnreachableFlow *ast.FlowNode
45+
file *ast.SourceFile
46+
bindFunc func(*ast.Node) bool
47+
unreachableFlow *ast.FlowNode
4948

5049
container *ast.Node
5150
thisContainer *ast.Node
@@ -122,7 +121,6 @@ func bindSourceFile(file *ast.SourceFile) {
122121
b.file = file
123122
b.inStrictMode = b.options().BindInStrictMode && !file.IsDeclarationFile || ast.IsExternalModule(file)
124123
b.unreachableFlow = b.newFlowNode(ast.FlowFlagsUnreachable)
125-
b.reportedUnreachableFlow = b.newFlowNode(ast.FlowFlagsUnreachable)
126124
b.bind(file.AsNode())
127125
file.SymbolCount = b.symbolCount
128126
file.ClassifiableNames = b.classifiableNames
@@ -1535,18 +1533,25 @@ func (b *Binder) bindChildren(node *ast.Node) {
15351533
// Most nodes aren't valid in an assignment pattern, so we clear the value here
15361534
// and set it before we descend into nodes that could actually be part of an assignment pattern.
15371535
b.inAssignmentPattern = false
1538-
if b.checkUnreachable(node) {
1536+
1537+
if b.currentFlow == b.unreachableFlow {
1538+
if flowNodeData := node.FlowNodeData(); flowNodeData != nil {
1539+
flowNodeData.FlowNode = nil
1540+
}
1541+
if ast.IsPotentiallyExecutableNode(node) {
1542+
node.Flags |= ast.NodeFlagsUnreachable
1543+
}
15391544
b.bindEachChild(node)
15401545
b.inAssignmentPattern = saveInAssignmentPattern
15411546
return
15421547
}
1543-
kind := node.Kind
1544-
if kind >= ast.KindFirstStatement && kind <= ast.KindLastStatement && (b.options().AllowUnreachableCode != core.TSTrue || kind == ast.KindReturnStatement) {
1545-
hasFlowNodeData := node.FlowNodeData()
1546-
if hasFlowNodeData != nil {
1547-
hasFlowNodeData.FlowNode = b.currentFlow
1548+
1549+
if ast.KindFirstStatement <= node.Kind && node.Kind <= ast.KindLastStatement {
1550+
if flowNodeData := node.FlowNodeData(); flowNodeData != nil {
1551+
flowNodeData.FlowNode = b.currentFlow
15481552
}
15491553
}
1554+
15501555
switch node.Kind {
15511556
case ast.KindWhileStatement:
15521557
b.bindWhileStatement(node)
@@ -1657,94 +1662,6 @@ func (b *Binder) bindEachStatementFunctionsFirst(statements *ast.NodeList) {
16571662
}
16581663
}
16591664

1660-
func (b *Binder) checkUnreachable(node *ast.Node) bool {
1661-
if b.currentFlow.Flags&ast.FlowFlagsUnreachable == 0 {
1662-
return false
1663-
}
1664-
if b.currentFlow == b.unreachableFlow {
1665-
// report errors on all statements except empty ones
1666-
// report errors on class declarations
1667-
// report errors on enums with preserved emit
1668-
// report errors on instantiated modules
1669-
reportError := ast.IsStatementButNotDeclaration(node) && !ast.IsEmptyStatement(node) ||
1670-
ast.IsClassDeclaration(node) ||
1671-
isEnumDeclarationWithPreservedEmit(node, b.options()) ||
1672-
ast.IsModuleDeclaration(node) && b.shouldReportErrorOnModuleDeclaration(node)
1673-
if reportError {
1674-
b.currentFlow = b.reportedUnreachableFlow
1675-
if b.options().AllowUnreachableCode != core.TSTrue {
1676-
// unreachable code is reported if
1677-
// - user has explicitly asked about it AND
1678-
// - statement is in not ambient context (statements in ambient context is already an error
1679-
// so we should not report extras) AND
1680-
// - node is not variable statement OR
1681-
// - node is block scoped variable statement OR
1682-
// - node is not block scoped variable statement and at least one variable declaration has initializer
1683-
// Rationale: we don't want to report errors on non-initialized var's since they are hoisted
1684-
// On the other side we do want to report errors on non-initialized 'lets' because of TDZ
1685-
isError := unreachableCodeIsError(b.options()) && node.Flags&ast.NodeFlagsAmbient == 0 && (!ast.IsVariableStatement(node) ||
1686-
ast.GetCombinedNodeFlags(node.AsVariableStatement().DeclarationList)&ast.NodeFlagsBlockScoped != 0 ||
1687-
core.Some(node.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool {
1688-
return d.Initializer() != nil
1689-
}))
1690-
b.errorOnEachUnreachableRange(node, isError)
1691-
}
1692-
}
1693-
}
1694-
return true
1695-
}
1696-
1697-
func (b *Binder) shouldReportErrorOnModuleDeclaration(node *ast.Node) bool {
1698-
instanceState := ast.GetModuleInstanceState(node)
1699-
return instanceState == ast.ModuleInstanceStateInstantiated || (instanceState == ast.ModuleInstanceStateConstEnumOnly && b.options().ShouldPreserveConstEnums)
1700-
}
1701-
1702-
func (b *Binder) errorOnEachUnreachableRange(node *ast.Node, isError bool) {
1703-
if b.isExecutableStatement(node) && ast.IsBlock(node.Parent) {
1704-
statements := node.Parent.Statements()
1705-
index := slices.Index(statements, node)
1706-
var first, last *ast.Node
1707-
for _, s := range statements[index:] {
1708-
if b.isExecutableStatement(s) {
1709-
if first == nil {
1710-
first = s
1711-
}
1712-
last = s
1713-
} else if first != nil {
1714-
b.errorOrSuggestionOnRange(isError, first, last, diagnostics.Unreachable_code_detected)
1715-
first = nil
1716-
}
1717-
}
1718-
if first != nil {
1719-
b.errorOrSuggestionOnRange(isError, first, last, diagnostics.Unreachable_code_detected)
1720-
}
1721-
} else {
1722-
b.errorOrSuggestionOnNode(isError, node, diagnostics.Unreachable_code_detected)
1723-
}
1724-
}
1725-
1726-
// As opposed to a pure declaration like an `interface`
1727-
func (b *Binder) isExecutableStatement(s *ast.Node) bool {
1728-
// Don't remove statements that can validly be used before they appear.
1729-
return !ast.IsFunctionDeclaration(s) && !b.isPurelyTypeDeclaration(s) && !(ast.IsVariableStatement(s) && ast.GetCombinedNodeFlags(s)&ast.NodeFlagsBlockScoped == 0 &&
1730-
core.Some(s.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes, func(d *ast.Node) bool {
1731-
return d.Initializer() == nil
1732-
}))
1733-
}
1734-
1735-
func (b *Binder) isPurelyTypeDeclaration(s *ast.Node) bool {
1736-
switch s.Kind {
1737-
case ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration:
1738-
return true
1739-
case ast.KindModuleDeclaration:
1740-
return ast.GetModuleInstanceState(s) != ast.ModuleInstanceStateInstantiated
1741-
case ast.KindEnumDeclaration:
1742-
return !isEnumDeclarationWithPreservedEmit(s, b.options())
1743-
default:
1744-
return false
1745-
}
1746-
}
1747-
17481665
func (b *Binder) setContinueTarget(node *ast.Node, target *ast.FlowLabel) *ast.FlowLabel {
17491666
label := b.activeLabelList
17501667
for label != nil && node.Parent.Kind == ast.KindLabeledStatement {
@@ -2131,8 +2048,9 @@ func (b *Binder) bindLabeledStatement(node *ast.Node) {
21312048
}
21322049
b.bind(stmt.Label)
21332050
b.bind(stmt.Statement)
2134-
if !b.activeLabelList.referenced && b.options().AllowUnusedLabels != core.TSTrue {
2135-
b.errorOrSuggestionOnNode(unusedLabelIsError(b.options()), stmt.Label, diagnostics.Unused_label)
2051+
if !b.activeLabelList.referenced {
2052+
// Mark the label as unused; the checker will decide whether to report it
2053+
stmt.Label.Flags |= ast.NodeFlagsUnreachable
21362054
}
21372055
b.activeLabelList = b.activeLabelList.next
21382056
b.addAntecedent(postStatementLabel, b.currentFlow)
@@ -2454,10 +2372,6 @@ func (b *Binder) bindInitializer(node *ast.Node) {
24542372
b.currentFlow = b.finishFlowLabel(exitFlow)
24552373
}
24562374

2457-
func isEnumDeclarationWithPreservedEmit(node *ast.Node, options core.SourceFileAffectingCompilerOptions) bool {
2458-
return node.Kind == ast.KindEnumDeclaration && (!ast.IsEnumConst(node) || options.ShouldPreserveConstEnums)
2459-
}
2460-
24612375
func setFlowNode(node *ast.Node, flowNode *ast.FlowNode) {
24622376
data := node.FlowNodeData()
24632377
if data != nil {
@@ -2749,14 +2663,6 @@ func isFunctionSymbol(symbol *ast.Symbol) bool {
27492663
return false
27502664
}
27512665

2752-
func unreachableCodeIsError(options core.SourceFileAffectingCompilerOptions) bool {
2753-
return options.AllowUnreachableCode == core.TSFalse
2754-
}
2755-
2756-
func unusedLabelIsError(options core.SourceFileAffectingCompilerOptions) bool {
2757-
return options.AllowUnusedLabels == core.TSFalse
2758-
}
2759-
27602666
func isStatementCondition(node *ast.Node) bool {
27612667
switch node.Parent.Kind {
27622668
case ast.KindIfStatement, ast.KindWhileStatement, ast.KindDoStatement:

internal/checker/checker.go

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,8 @@ type Checker struct {
857857
activeTypeMappersCaches []map[string]*Type
858858
ambientModulesOnce sync.Once
859859
ambientModules []*ast.Symbol
860+
withinUnreachableCode bool
861+
reportedUnreachableNodes collections.Set[*ast.Node]
860862

861863
mu sync.Mutex
862864
}
@@ -2144,6 +2146,7 @@ func (c *Checker) checkSourceFile(ctx context.Context, sourceFile *ast.SourceFil
21442146
c.wasCanceled = true
21452147
}
21462148
c.ctx = nil
2149+
c.reportedUnreachableNodes.Clear()
21472150
links.typeChecked = true
21482151
}
21492152
}
@@ -2160,10 +2163,12 @@ func (c *Checker) checkSourceElements(nodes []*ast.Node) {
21602163
func (c *Checker) checkSourceElement(node *ast.Node) bool {
21612164
if node != nil {
21622165
saveCurrentNode := c.currentNode
2166+
saveWithinUnreachableCode := c.withinUnreachableCode
21632167
c.currentNode = node
21642168
c.instantiationCount = 0
21652169
c.checkSourceElementWorker(node)
21662170
c.currentNode = saveCurrentNode
2171+
c.withinUnreachableCode = saveWithinUnreachableCode
21672172
}
21682173
return false
21692174
}
@@ -2179,13 +2184,13 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) {
21792184
}
21802185
}
21812186
}
2182-
kind := node.Kind
2183-
if kind >= ast.KindFirstStatement && kind <= ast.KindLastStatement {
2184-
flowNode := node.FlowNodeData().FlowNode
2185-
if flowNode != nil && !c.isReachableFlowNode(flowNode) {
2186-
c.errorOrSuggestion(c.compilerOptions.AllowUnreachableCode == core.TSFalse, node, diagnostics.Unreachable_code_detected)
2187+
2188+
if !c.withinUnreachableCode && c.compilerOptions.AllowUnreachableCode != core.TSTrue {
2189+
if c.checkSourceElementUnreachable(node) {
2190+
c.withinUnreachableCode = true
21872191
}
21882192
}
2193+
21892194
switch node.Kind {
21902195
case ast.KindTypeParameter:
21912196
c.checkTypeParameter(node)
@@ -2308,6 +2313,87 @@ func (c *Checker) checkSourceElementWorker(node *ast.Node) {
23082313
}
23092314
}
23102315

2316+
func (c *Checker) checkSourceElementUnreachable(node *ast.Node) bool {
2317+
if !ast.IsPotentiallyExecutableNode(node) {
2318+
return false
2319+
}
2320+
2321+
if c.reportedUnreachableNodes.Has(node) {
2322+
return true
2323+
}
2324+
2325+
if !c.isSourceElementUnreachable(node) {
2326+
return false
2327+
}
2328+
2329+
c.reportedUnreachableNodes.Add(node)
2330+
2331+
sourceFile := ast.GetSourceFileOfNode(node)
2332+
2333+
start := node.Pos()
2334+
end := node.End()
2335+
2336+
parent := node.Parent
2337+
if parent.CanHaveStatements() {
2338+
statements := parent.Statements()
2339+
if offset := slices.Index(statements, node); offset >= 0 {
2340+
// Scan backwards to find the first unreachable unreported node;
2341+
// this may happen when producing region diagnostics where not all nodes
2342+
// will have been visited.
2343+
// TODO: enable this code once we support region diagnostics again.
2344+
first := offset
2345+
// for i := offset - 1; i >= 0; i-- {
2346+
// prevNode := statements[i]
2347+
// if !ast.IsPotentiallyExecutableNode(prevNode) || c.reportedUnreachableNodes.Has(prevNode) || !c.isSourceElementUnreachable(prevNode) {
2348+
// break
2349+
// }
2350+
// firstUnreachableIndex = i
2351+
// c.reportedUnreachableNodes.Add(prevNode)
2352+
// }
2353+
2354+
last := offset
2355+
for i := offset + 1; i < len(statements); i++ {
2356+
nextNode := statements[i]
2357+
if !ast.IsPotentiallyExecutableNode(nextNode) || !c.isSourceElementUnreachable(nextNode) {
2358+
break
2359+
}
2360+
last = i
2361+
c.reportedUnreachableNodes.Add(nextNode)
2362+
}
2363+
2364+
start = statements[first].Pos()
2365+
end = statements[last].End()
2366+
}
2367+
}
2368+
2369+
start = scanner.SkipTrivia(sourceFile.Text(), start)
2370+
2371+
diagnostic := ast.NewDiagnostic(sourceFile, core.NewTextRange(start, end), diagnostics.Unreachable_code_detected)
2372+
c.addErrorOrSuggestion(c.compilerOptions.AllowUnreachableCode == core.TSFalse, diagnostic)
2373+
2374+
return true
2375+
}
2376+
2377+
func (c *Checker) isSourceElementUnreachable(node *ast.Node) bool {
2378+
// Precondition: ast.IsPotentiallyExecutableNode is true
2379+
if node.Flags&ast.NodeFlagsUnreachable != 0 {
2380+
// The binder has determined that this code is unreachable.
2381+
// Ignore const enums unless preserveConstEnums is set.
2382+
switch node.Kind {
2383+
case ast.KindEnumDeclaration:
2384+
return !ast.IsEnumConst(node) || c.compilerOptions.ShouldPreserveConstEnums()
2385+
case ast.KindModuleDeclaration:
2386+
return ast.IsInstantiatedModule(node, c.compilerOptions.ShouldPreserveConstEnums())
2387+
default:
2388+
return true
2389+
}
2390+
} else if flowNode := node.FlowNodeData().FlowNode; flowNode != nil {
2391+
// For code the binder doesn't know is unreachable, use control flow / types.
2392+
return !c.isReachableFlowNode(flowNode)
2393+
}
2394+
return false
2395+
}
2396+
23112397
// Function and class expression bodies are checked after all statements in the enclosing body. This is
23122398
// to ensure constructs like the following are permitted:
23132399
//
@@ -4022,6 +4108,9 @@ func (c *Checker) checkLabeledStatement(node *ast.Node) {
40224108
}
40234109
}
40244110
}
4111+
if labelNode.Flags&ast.NodeFlagsUnreachable != 0 && c.compilerOptions.AllowUnusedLabels != core.TSTrue {
4112+
c.errorOrSuggestion(c.compilerOptions.AllowUnusedLabels == core.TSFalse, labelNode, diagnostics.Unused_label)
4113+
}
40254114
c.checkSourceElement(labeledStatement.Statement)
40264115
}
40274116

0 commit comments

Comments
 (0)