David López 37e6995b45 lintpack/gocritic: update lintpack & gocritic versions
update lintpack & gocritic versions to support all the new gocritic checks
2018-12-22 15:34:16 +03:00

170 lines
4.5 KiB
Go

package checkers
import (
"go/ast"
"go/token"
"github.com/go-lintpack/lintpack"
"github.com/go-lintpack/lintpack/astwalk"
)
func init() {
var info lintpack.CheckerInfo
info.Name = "unlabelStmt"
info.Tags = []string{"style", "experimental"}
info.Summary = "Detects redundant statement labels"
info.Before = `
derp:
for x := range xs {
if x == 0 {
break derp
}
}`
info.After = `
for x := range xs {
if x == 0 {
break
}
}`
collection.AddChecker(&info, func(ctx *lintpack.CheckerContext) lintpack.FileWalker {
return astwalk.WalkerForStmt(&unlabelStmtChecker{ctx: ctx})
})
}
type unlabelStmtChecker struct {
astwalk.WalkHandler
ctx *lintpack.CheckerContext
}
func (c *unlabelStmtChecker) EnterFunc(fn *ast.FuncDecl) bool {
if fn.Body == nil {
return false
}
// TODO(Quasilyte): should not do additional traversal here.
// For now, skip all functions that contain goto statement.
return !containsNode(fn.Body, func(n ast.Node) bool {
br, ok := n.(*ast.BranchStmt)
return ok && br.Tok == token.GOTO
})
}
func (c *unlabelStmtChecker) VisitStmt(stmt ast.Stmt) {
labeled, ok := stmt.(*ast.LabeledStmt)
if !ok || !c.canBreakFrom(labeled.Stmt) {
return
}
// We have a labeled statement from that have labeled continue/break.
// This is an invariant, since unused label is a compile-time error
// and we're currently skipping functions containing goto.
//
// Also note that Go labels are function-scoped and there
// can be no re-definitions. This means that we don't
// need to care about label shadowing or things like that.
//
// The task is to find cases where labeled branch (continue/break)
// is redundant and can be re-written, decreasing the label usages
// and potentially leading to its redundancy,
// or finding the redundant labels right away.
name := labeled.Label.Name
// Simplest case that can prove that label is redundant.
//
// If labeled branch is somewhere inside the statement block itself
// and none of the nested break'able statements refer to that label,
// the label can be removed.
matchUsage := func(n ast.Node) bool {
return c.canBreakFrom(n) && c.usesLabel(c.blockStmtOf(n), name)
}
if !containsNode(c.blockStmtOf(labeled.Stmt), matchUsage) {
c.warnRedundant(labeled)
return
}
// Only for loops: if last stmt in list is a loop
// that contains labeled "continue" to the outer loop label,
// it can be refactored to use "break" instead.
if c.isLoop(labeled.Stmt) {
body := c.blockStmtOf(labeled.Stmt)
if len(body.List) == 0 {
return
}
last := body.List[len(body.List)-1]
if !c.isLoop(last) {
return
}
br := findNode(c.blockStmtOf(last), func(n ast.Node) bool {
br, ok := n.(*ast.BranchStmt)
return ok && br.Label != nil &&
br.Label.Name == name && br.Tok == token.CONTINUE
})
if br != nil {
c.warnLabeledContinue(br, name)
}
}
}
// isLoop reports whether n is a loop of some kind.
// In other words, it tells whether n body can contain "continue"
// associated with n.
func (c *unlabelStmtChecker) isLoop(n ast.Node) bool {
switch n.(type) {
case *ast.ForStmt, *ast.RangeStmt:
return true
default:
return false
}
}
// canBreakFrom reports whether it is possible to "break" or "continue" from n body.
func (c *unlabelStmtChecker) canBreakFrom(n ast.Node) bool {
switch n.(type) {
case *ast.RangeStmt, *ast.ForStmt, *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt:
return true
default:
return false
}
}
// blockStmtOf returns body of specified node.
//
// TODO(quasilyte): handle other statements and see if it can be useful
// in other checkers.
func (c *unlabelStmtChecker) blockStmtOf(n ast.Node) *ast.BlockStmt {
switch n := n.(type) {
case *ast.RangeStmt:
return n.Body
case *ast.ForStmt:
return n.Body
case *ast.SwitchStmt:
return n.Body
case *ast.TypeSwitchStmt:
return n.Body
case *ast.SelectStmt:
return n.Body
default:
return nil
}
}
// usesLabel reports whether n contains a usage of label.
func (c *unlabelStmtChecker) usesLabel(n *ast.BlockStmt, label string) bool {
return containsNode(n, func(n ast.Node) bool {
branch, ok := n.(*ast.BranchStmt)
return ok && branch.Label != nil &&
branch.Label.Name == label &&
(branch.Tok == token.CONTINUE || branch.Tok == token.BREAK)
})
}
func (c *unlabelStmtChecker) warnRedundant(cause *ast.LabeledStmt) {
c.ctx.Warn(cause, "label %s is redundant", cause.Label)
}
func (c *unlabelStmtChecker) warnLabeledContinue(cause ast.Node, label string) {
c.ctx.Warn(cause, "change `continue %s` to `break`", label)
}