mirror of
https://github.com/golang/go
synced 2024-11-18 15:34:53 -07:00
9d6cc5fd08
LGTM=r R=r CC=golang-codereviews https://golang.org/cl/107260043
297 lines
6.6 KiB
Go
297 lines
6.6 KiB
Go
// 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.
|
|
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
|
|
}
|
|
}
|