// Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package unreachable defines an Analyzer that checks for unreachable code. package unreachable // TODO(adonovan): use the new cfg package, which is more precise. import ( "go/ast" "go/token" "log" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" ) const Doc = `check for unreachable code The unreachable analyzer finds statements that execution can never reach because they are preceded by an return statement, a call to panic, an infinite loop, or similar constructs.` var Analyzer = &analysis.Analyzer{ Name: "unreachable", Doc: Doc, Requires: []*analysis.Analyzer{inspect.Analyzer}, RunDespiteErrors: true, Run: run, } func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.FuncDecl)(nil), (*ast.FuncLit)(nil), } inspect.Preorder(nodeFilter, func(n ast.Node) { var body *ast.BlockStmt switch n := n.(type) { case *ast.FuncDecl: body = n.Body case *ast.FuncLit: body = n.Body } if body == nil { return } d := &deadState{ pass: pass, hasBreak: make(map[ast.Stmt]bool), hasGoto: make(map[string]bool), labels: make(map[string]ast.Stmt), } d.findLabels(body) d.reachable = true d.findDead(body) }) return nil, nil } type deadState struct { pass *analysis.Pass hasBreak map[ast.Stmt]bool hasGoto map[string]bool labels map[string]ast.Stmt breakTarget ast.Stmt reachable bool } // findLabels gathers information about the labels defined and used by stmt // and about which statements break, whether a label is involved or not. func (d *deadState) findLabels(stmt ast.Stmt) { switch x := stmt.(type) { default: log.Fatalf("%s: internal error in findLabels: unexpected statement %T", d.pass.Fset.Position(x.Pos()), x) case *ast.AssignStmt, *ast.BadStmt, *ast.DeclStmt, *ast.DeferStmt, *ast.EmptyStmt, *ast.ExprStmt, *ast.GoStmt, *ast.IncDecStmt, *ast.ReturnStmt, *ast.SendStmt: // no statements inside case *ast.BlockStmt: for _, stmt := range x.List { d.findLabels(stmt) } case *ast.BranchStmt: switch x.Tok { case token.GOTO: if x.Label != nil { d.hasGoto[x.Label.Name] = true } case token.BREAK: stmt := d.breakTarget if x.Label != nil { stmt = d.labels[x.Label.Name] } if stmt != nil { d.hasBreak[stmt] = true } } case *ast.IfStmt: d.findLabels(x.Body) if x.Else != nil { d.findLabels(x.Else) } case *ast.LabeledStmt: d.labels[x.Label.Name] = x.Stmt d.findLabels(x.Stmt) // These cases are all the same, but the x.Body only works // when the specific type of x is known, so the cases cannot // be merged. case *ast.ForStmt: outer := d.breakTarget d.breakTarget = x d.findLabels(x.Body) d.breakTarget = outer case *ast.RangeStmt: outer := d.breakTarget d.breakTarget = x d.findLabels(x.Body) d.breakTarget = outer case *ast.SelectStmt: outer := d.breakTarget d.breakTarget = x d.findLabels(x.Body) d.breakTarget = outer case *ast.SwitchStmt: outer := d.breakTarget d.breakTarget = x d.findLabels(x.Body) d.breakTarget = outer case *ast.TypeSwitchStmt: outer := d.breakTarget d.breakTarget = x d.findLabels(x.Body) d.breakTarget = outer case *ast.CommClause: for _, stmt := range x.Body { d.findLabels(stmt) } case *ast.CaseClause: for _, stmt := range x.Body { d.findLabels(stmt) } } } // findDead walks the statement looking for dead code. // If d.reachable is false on entry, stmt itself is dead. // When findDead returns, d.reachable tells whether the // statement following stmt is reachable. func (d *deadState) findDead(stmt ast.Stmt) { // Is this a labeled goto target? // If so, assume it is reachable due to the goto. // This is slightly conservative, in that we don't // check that the goto is reachable, so // L: goto L // will not provoke a warning. // But it's good enough. if x, isLabel := stmt.(*ast.LabeledStmt); isLabel && d.hasGoto[x.Label.Name] { d.reachable = true } if !d.reachable { switch stmt.(type) { case *ast.EmptyStmt: // do not warn about unreachable empty statements default: d.pass.Report(analysis.Diagnostic{ Pos: stmt.Pos(), End: stmt.End(), Message: "unreachable code", SuggestedFixes: []analysis.SuggestedFix{{ Message: "Remove", TextEdits: []analysis.TextEdit{{ Pos: stmt.Pos(), End: stmt.End(), }}, }}, }) d.reachable = true // silence error about next statement } } switch x := stmt.(type) { default: log.Fatalf("%s: internal error in findDead: unexpected statement %T", d.pass.Fset.Position(x.Pos()), x) case *ast.AssignStmt, *ast.BadStmt, *ast.DeclStmt, *ast.DeferStmt, *ast.EmptyStmt, *ast.GoStmt, *ast.IncDecStmt, *ast.SendStmt: // no control flow case *ast.BlockStmt: for _, stmt := range x.List { d.findDead(stmt) } case *ast.BranchStmt: switch x.Tok { case token.BREAK, token.GOTO, token.FALLTHROUGH: d.reachable = false case token.CONTINUE: // NOTE: We accept "continue" statements as terminating. // They are not necessary in the spec definition of terminating, // because a continue statement cannot be the final statement // before a return. But for the more general problem of syntactically // identifying dead code, continue redirects control flow just // like the other terminating statements. d.reachable = false } case *ast.ExprStmt: // Call to panic? call, ok := x.X.(*ast.CallExpr) if ok { name, ok := call.Fun.(*ast.Ident) if ok && name.Name == "panic" && name.Obj == nil { d.reachable = false } } case *ast.ForStmt: d.findDead(x.Body) d.reachable = x.Cond != nil || d.hasBreak[x] case *ast.IfStmt: d.findDead(x.Body) if x.Else != nil { r := d.reachable d.reachable = true d.findDead(x.Else) d.reachable = d.reachable || r } else { // might not have executed if statement d.reachable = true } case *ast.LabeledStmt: d.findDead(x.Stmt) case *ast.RangeStmt: d.findDead(x.Body) d.reachable = true case *ast.ReturnStmt: d.reachable = false case *ast.SelectStmt: // NOTE: Unlike switch and type switch below, we don't care // whether a select has a default, because a select without a // default blocks until one of the cases can run. That's different // from a switch without a default, which behaves like it has // a default with an empty body. anyReachable := false for _, comm := range x.Body.List { d.reachable = true for _, stmt := range comm.(*ast.CommClause).Body { d.findDead(stmt) } anyReachable = anyReachable || d.reachable } d.reachable = anyReachable || d.hasBreak[x] case *ast.SwitchStmt: anyReachable := false hasDefault := false for _, cas := range x.Body.List { cc := cas.(*ast.CaseClause) if cc.List == nil { hasDefault = true } d.reachable = true for _, stmt := range cc.Body { d.findDead(stmt) } anyReachable = anyReachable || d.reachable } d.reachable = anyReachable || d.hasBreak[x] || !hasDefault case *ast.TypeSwitchStmt: anyReachable := false hasDefault := false for _, cas := range x.Body.List { cc := cas.(*ast.CaseClause) if cc.List == nil { hasDefault = true } d.reachable = true for _, stmt := range cc.Body { d.findDead(stmt) } anyReachable = anyReachable || d.reachable } d.reachable = anyReachable || d.hasBreak[x] || !hasDefault } }