mirror of
https://github.com/golang/go
synced 2024-11-06 01:46:12 -07:00
f895b43688
This removes much of the AST logic out of main.go, and makes it easier to build custom vet binaries The trade-off in this change is for flexibility. There's very little change in the per-check files, a lot less code in main.go (specifically the AST walking logic has shrunk), and it makes it much easier to build custom vet binaries simply by dropping new source files in the directory. LGTM=josharian, r R=r, josharian, kamil.kisiel CC=golang-codereviews https://golang.org/cl/83400043
295 lines
6.6 KiB
Go
295 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:
|
|
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
|
|
}
|
|
}
|