170 lines
4.5 KiB
Go
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)
|
|
}
|