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