| // 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. |
| |
| // Check for syntactically unreachable code. |
| |
| package main |
| |
| import ( |
| "go/ast" |
| "go/token" |
| ) |
| |
| func init() { |
| register("unreachable", |
| "check for unreachable code", |
| checkUnreachable, |
| funcDecl, funcLit) |
| } |
| |
| type deadState struct { |
| f *File |
| hasBreak map[ast.Stmt]bool |
| hasGoto map[string]bool |
| labels map[string]ast.Stmt |
| breakTarget ast.Stmt |
| |
| reachable bool |
| } |
| |
| // checkUnreachable checks a function body for dead code. |
| // |
| // TODO(adonovan): use the new cfg package, which is more precise. |
| func checkUnreachable(f *File, node ast.Node) { |
| var body *ast.BlockStmt |
| switch n := node.(type) { |
| case *ast.FuncDecl: |
| body = n.Body |
| case *ast.FuncLit: |
| body = n.Body |
| } |
| if body == nil { |
| return |
| } |
| |
| d := &deadState{ |
| f: f, |
| 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) |
| } |
| |
| // 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: |
| d.f.Warnf(x.Pos(), "internal error in findLabels: unexpected statement %T", 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.f.Bad(stmt.Pos(), "unreachable code") |
| d.reachable = true // silence error about next statement |
| } |
| } |
| |
| switch x := stmt.(type) { |
| default: |
| d.f.Warnf(x.Pos(), "internal error in findDead: unexpected statement %T", 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 |
| } |
| } |