1
0
mirror of https://github.com/mgechev/revive.git synced 2024-11-28 08:49:11 +02:00
revive/rule/early-return.go

177 lines
3.9 KiB
Go

package rule
import (
"fmt"
"go/ast"
"go/token"
"github.com/mgechev/revive/lint"
)
// EarlyReturnRule finds opportunities to reduce nesting by inverting
// the condition of an "if" block.
type EarlyReturnRule struct{}
// Apply applies the rule to given file.
func (*EarlyReturnRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
var failures []lint.Failure
onFailure := func(failure lint.Failure) {
failures = append(failures, failure)
}
w := lintEarlyReturnRule{onFailure: onFailure}
ast.Walk(w, file.AST)
return failures
}
// Name returns the rule name.
func (*EarlyReturnRule) Name() string {
return "early-return"
}
type lintEarlyReturnRule struct {
onFailure func(lint.Failure)
}
func (w lintEarlyReturnRule) Visit(node ast.Node) ast.Visitor {
ifStmt, ok := node.(*ast.IfStmt)
if !ok {
return w
}
w.visitIf(ifStmt, false, false)
return nil
}
func (w lintEarlyReturnRule) visitIf(ifStmt *ast.IfStmt, hasNonReturnBranch, hasIfInitializer bool) {
// look for other if-else chains nested inside this if { } block
ast.Walk(w, ifStmt.Body)
if ifStmt.Else == nil {
// no else branch
return
}
if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
hasIfInitializer = true
}
bodyFlow := w.branchFlow(ifStmt.Body)
switch elseBlock := ifStmt.Else.(type) {
case *ast.IfStmt:
if bodyFlow.canFlowIntoNext() {
hasNonReturnBranch = true
}
w.visitIf(elseBlock, hasNonReturnBranch, hasIfInitializer)
case *ast.BlockStmt:
// look for other if-else chains nested inside this else { } block
ast.Walk(w, elseBlock)
if hasNonReturnBranch && bodyFlow != branchFlowEmpty {
// if we de-indent this block then a previous branch
// might flow into it, affecting program behaviour
return
}
if !bodyFlow.canFlowIntoNext() {
// avoid overlapping with superfluous-else
return
}
elseFlow := w.branchFlow(elseBlock)
if !elseFlow.canFlowIntoNext() {
failMsg := fmt.Sprintf("if c {%[1]s } else {%[2]s } can be simplified to if !c {%[2]s }%[1]s",
bodyFlow, elseFlow)
if hasIfInitializer {
// if statement has a := initializer, so we might need to move the assignment
// onto its own line in case the body references it
failMsg += " (move short variable declaration to its own line if necessary)"
}
w.onFailure(lint.Failure{
Confidence: 1,
Node: ifStmt,
Failure: failMsg,
})
}
default:
panic("invalid node type for else")
}
}
type branchFlowKind int
const (
branchFlowEmpty branchFlowKind = iota
branchFlowReturn
branchFlowPanic
branchFlowContinue
branchFlowBreak
branchFlowGoto
branchFlowRegular
)
func (w lintEarlyReturnRule) branchFlow(block *ast.BlockStmt) branchFlowKind {
blockLen := len(block.List)
if blockLen == 0 {
return branchFlowEmpty
}
switch stmt := block.List[blockLen-1].(type) {
case *ast.ReturnStmt:
return branchFlowReturn
case *ast.BlockStmt:
return w.branchFlow(stmt)
case *ast.BranchStmt:
switch stmt.Tok {
case token.BREAK:
return branchFlowBreak
case token.CONTINUE:
return branchFlowContinue
case token.GOTO:
return branchFlowGoto
}
case *ast.ExprStmt:
if call, ok := stmt.X.(*ast.CallExpr); ok && isIdent(call.Fun, "panic") {
return branchFlowPanic
}
}
return branchFlowRegular
}
// Whether this branch's control can flow into the next statement following the if-else chain
func (k branchFlowKind) canFlowIntoNext() bool {
switch k {
case branchFlowReturn, branchFlowPanic, branchFlowContinue, branchFlowBreak, branchFlowGoto:
return false
default:
return true
}
}
func (k branchFlowKind) String() string {
switch k {
case branchFlowEmpty:
return ""
case branchFlowReturn:
return " ... return"
case branchFlowPanic:
return " ... panic()"
case branchFlowContinue:
return " ... continue"
case branchFlowBreak:
return " ... break"
case branchFlowGoto:
return " ... goto"
case branchFlowRegular:
return " ..."
default:
panic("invalid kind")
}
}