mirror of
https://github.com/mgechev/revive.git
synced 2025-02-01 13:07:44 +02:00
refactor: extract shared code for linting if-else chains (#821)
* refactor: extract shared code for linting if-else chains The rules "early-return", "indent-error-flow" and "superfluous-else" have a similar structure. This moves the common logic for classifying if-else chains to a common package. A few side benefits: - "early-return" now handles os.Exit/log.Panicf/etc - "superfluous-else" now handles (builtin) panic - "superfluous-else" and "indent-error-flow" now handle if/else chains with 2+ "if" branches * internal/ifelse: style fixes, renames, spelling
This commit is contained in:
parent
81d85b505d
commit
4bb48df5d2
68
internal/ifelse/branch.go
Normal file
68
internal/ifelse/branch.go
Normal file
@ -0,0 +1,68 @@
|
||||
package ifelse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
)
|
||||
|
||||
// Branch contains information about a branch within an if-else chain.
|
||||
type Branch struct {
|
||||
BranchKind
|
||||
Call // The function called at the end for kind Panic or Exit.
|
||||
}
|
||||
|
||||
// BlockBranch gets the Branch of an ast.BlockStmt.
|
||||
func BlockBranch(block *ast.BlockStmt) Branch {
|
||||
blockLen := len(block.List)
|
||||
if blockLen == 0 {
|
||||
return Branch{BranchKind: Empty}
|
||||
}
|
||||
|
||||
switch stmt := block.List[blockLen-1].(type) {
|
||||
case *ast.ReturnStmt:
|
||||
return Branch{BranchKind: Return}
|
||||
case *ast.BlockStmt:
|
||||
return BlockBranch(stmt)
|
||||
case *ast.BranchStmt:
|
||||
switch stmt.Tok {
|
||||
case token.BREAK:
|
||||
return Branch{BranchKind: Break}
|
||||
case token.CONTINUE:
|
||||
return Branch{BranchKind: Continue}
|
||||
case token.GOTO:
|
||||
return Branch{BranchKind: Goto}
|
||||
}
|
||||
case *ast.ExprStmt:
|
||||
fn, ok := ExprCall(stmt)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
kind, ok := DeviatingFuncs[fn]
|
||||
if ok {
|
||||
return Branch{BranchKind: kind, Call: fn}
|
||||
}
|
||||
}
|
||||
|
||||
return Branch{BranchKind: Regular}
|
||||
}
|
||||
|
||||
// String returns a brief string representation
|
||||
func (b Branch) String() string {
|
||||
switch b.BranchKind {
|
||||
case Panic, Exit:
|
||||
return fmt.Sprintf("... %v()", b.Call)
|
||||
default:
|
||||
return b.BranchKind.String()
|
||||
}
|
||||
}
|
||||
|
||||
// LongString returns a longer form string representation
|
||||
func (b Branch) LongString() string {
|
||||
switch b.BranchKind {
|
||||
case Panic, Exit:
|
||||
return fmt.Sprintf("call to %v function", b.Call)
|
||||
default:
|
||||
return b.BranchKind.LongString()
|
||||
}
|
||||
}
|
98
internal/ifelse/branch_kind.go
Normal file
98
internal/ifelse/branch_kind.go
Normal file
@ -0,0 +1,98 @@
|
||||
package ifelse
|
||||
|
||||
// BranchKind is a classifier for if-else branches. It says whether the branch is empty,
|
||||
// and whether the branch ends with a statement that deviates control flow.
|
||||
type BranchKind int
|
||||
|
||||
const (
|
||||
// Empty branches do nothing
|
||||
Empty BranchKind = iota
|
||||
|
||||
// Return branches return from the current function
|
||||
Return
|
||||
|
||||
// Continue branches continue a surrounding "for" loop
|
||||
Continue
|
||||
|
||||
// Break branches break a surrounding "for" loop
|
||||
Break
|
||||
|
||||
// Goto branches conclude with a "goto" statement
|
||||
Goto
|
||||
|
||||
// Panic branches panic the current function
|
||||
Panic
|
||||
|
||||
// Exit branches end the program
|
||||
Exit
|
||||
|
||||
// Regular branches do not fit any category above
|
||||
Regular
|
||||
)
|
||||
|
||||
// IsEmpty tests if the branch is empty
|
||||
func (k BranchKind) IsEmpty() bool { return k == Empty }
|
||||
|
||||
// Returns tests if the branch returns from the current function
|
||||
func (k BranchKind) Returns() bool { return k == Return }
|
||||
|
||||
// Deviates tests if the control does not flow to the first
|
||||
// statement following the if-else chain.
|
||||
func (k BranchKind) Deviates() bool {
|
||||
switch k {
|
||||
case Empty, Regular:
|
||||
return false
|
||||
case Return, Continue, Break, Goto, Panic, Exit:
|
||||
return true
|
||||
default:
|
||||
panic("invalid kind")
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a brief string representation
|
||||
func (k BranchKind) String() string {
|
||||
switch k {
|
||||
case Empty:
|
||||
return ""
|
||||
case Regular:
|
||||
return "..."
|
||||
case Return:
|
||||
return "... return"
|
||||
case Continue:
|
||||
return "... continue"
|
||||
case Break:
|
||||
return "... break"
|
||||
case Goto:
|
||||
return "... goto"
|
||||
case Panic:
|
||||
return "... panic()"
|
||||
case Exit:
|
||||
return "... os.Exit()"
|
||||
default:
|
||||
panic("invalid kind")
|
||||
}
|
||||
}
|
||||
|
||||
// LongString returns a longer form string representation
|
||||
func (k BranchKind) LongString() string {
|
||||
switch k {
|
||||
case Empty:
|
||||
return "an empty block"
|
||||
case Regular:
|
||||
return "a regular statement"
|
||||
case Return:
|
||||
return "a return statement"
|
||||
case Continue:
|
||||
return "a continue statement"
|
||||
case Break:
|
||||
return "a break statement"
|
||||
case Goto:
|
||||
return "a goto statement"
|
||||
case Panic:
|
||||
return "a function call that panics"
|
||||
case Exit:
|
||||
return "a function call that exits the program"
|
||||
default:
|
||||
panic("invalid kind")
|
||||
}
|
||||
}
|
9
internal/ifelse/chain.go
Normal file
9
internal/ifelse/chain.go
Normal file
@ -0,0 +1,9 @@
|
||||
package ifelse
|
||||
|
||||
// Chain contains information about an if-else chain.
|
||||
type Chain struct {
|
||||
If Branch // what happens at the end of the "if" block
|
||||
Else Branch // what happens at the end of the "else" block
|
||||
HasInitializer bool // is there an "if"-initializer somewhere in the chain?
|
||||
HasPriorNonDeviating bool // is there a prior "if" block that does NOT deviate control flow?
|
||||
}
|
6
internal/ifelse/doc.go
Normal file
6
internal/ifelse/doc.go
Normal file
@ -0,0 +1,6 @@
|
||||
// Package ifelse provides helpers for analysing the control flow in if-else chains,
|
||||
// presently used by the following rules:
|
||||
// - early-return
|
||||
// - indent-error-flow
|
||||
// - superfluous-else
|
||||
package ifelse
|
51
internal/ifelse/func.go
Normal file
51
internal/ifelse/func.go
Normal file
@ -0,0 +1,51 @@
|
||||
package ifelse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
)
|
||||
|
||||
// Call contains the name of a function that deviates control flow.
|
||||
type Call struct {
|
||||
Pkg string // The package qualifier of the function, if not built-in.
|
||||
Name string // The function name.
|
||||
}
|
||||
|
||||
// DeviatingFuncs lists known control flow deviating function calls.
|
||||
var DeviatingFuncs = map[Call]BranchKind{
|
||||
{"os", "Exit"}: Exit,
|
||||
{"log", "Fatal"}: Exit,
|
||||
{"log", "Fatalf"}: Exit,
|
||||
{"log", "Fatalln"}: Exit,
|
||||
{"", "panic"}: Panic,
|
||||
{"log", "Panic"}: Panic,
|
||||
{"log", "Panicf"}: Panic,
|
||||
{"log", "Panicln"}: Panic,
|
||||
}
|
||||
|
||||
// ExprCall gets the Call of an ExprStmt, if any.
|
||||
func ExprCall(expr *ast.ExprStmt) (Call, bool) {
|
||||
call, ok := expr.X.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return Call{}, false
|
||||
}
|
||||
switch v := call.Fun.(type) {
|
||||
case *ast.Ident:
|
||||
return Call{Name: v.Name}, true
|
||||
case *ast.SelectorExpr:
|
||||
if ident, ok := v.X.(*ast.Ident); ok {
|
||||
return Call{Name: v.Sel.Name, Pkg: ident.Name}, true
|
||||
}
|
||||
}
|
||||
return Call{}, false
|
||||
}
|
||||
|
||||
// String returns the function name with package qualifier (if any)
|
||||
func (f Call) String() string {
|
||||
switch {
|
||||
case f.Pkg != "":
|
||||
return fmt.Sprintf("%s.%s", f.Pkg, f.Name)
|
||||
default:
|
||||
return f.Name
|
||||
}
|
||||
}
|
92
internal/ifelse/rule.go
Normal file
92
internal/ifelse/rule.go
Normal file
@ -0,0 +1,92 @@
|
||||
package ifelse
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
"github.com/mgechev/revive/lint"
|
||||
)
|
||||
|
||||
// Rule is an interface for linters operating on if-else chains
|
||||
type Rule interface {
|
||||
CheckIfElse(chain Chain) (failMsg string)
|
||||
}
|
||||
|
||||
// Apply evaluates the given Rule on if-else chains found within the given AST,
|
||||
// and returns the failures.
|
||||
//
|
||||
// Note that in if-else chain with multiple "if" blocks, only the *last* one is checked,
|
||||
// that is to say, given:
|
||||
//
|
||||
// if foo {
|
||||
// ...
|
||||
// } else if bar {
|
||||
// ...
|
||||
// } else {
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// Only the block following "bar" is linted. This is because the rules that use this function
|
||||
// do not presently have anything to say about earlier blocks in the chain.
|
||||
func Apply(rule Rule, node ast.Node, target Target) []lint.Failure {
|
||||
v := &visitor{rule: rule, target: target}
|
||||
ast.Walk(v, node)
|
||||
return v.failures
|
||||
}
|
||||
|
||||
type visitor struct {
|
||||
failures []lint.Failure
|
||||
target Target
|
||||
rule Rule
|
||||
}
|
||||
|
||||
func (v *visitor) Visit(node ast.Node) ast.Visitor {
|
||||
ifStmt, ok := node.(*ast.IfStmt)
|
||||
if !ok {
|
||||
return v
|
||||
}
|
||||
|
||||
v.visitChain(ifStmt, Chain{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *visitor) visitChain(ifStmt *ast.IfStmt, chain Chain) {
|
||||
// look for other if-else chains nested inside this if { } block
|
||||
ast.Walk(v, ifStmt.Body)
|
||||
|
||||
if ifStmt.Else == nil {
|
||||
// no else branch
|
||||
return
|
||||
}
|
||||
|
||||
if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
|
||||
chain.HasInitializer = true
|
||||
}
|
||||
chain.If = BlockBranch(ifStmt.Body)
|
||||
|
||||
switch elseBlock := ifStmt.Else.(type) {
|
||||
case *ast.IfStmt:
|
||||
if !chain.If.Deviates() {
|
||||
chain.HasPriorNonDeviating = true
|
||||
}
|
||||
v.visitChain(elseBlock, chain)
|
||||
case *ast.BlockStmt:
|
||||
// look for other if-else chains nested inside this else { } block
|
||||
ast.Walk(v, elseBlock)
|
||||
chain.Else = BlockBranch(elseBlock)
|
||||
if failMsg := v.rule.CheckIfElse(chain); failMsg != "" {
|
||||
if chain.HasInitializer {
|
||||
// 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)"
|
||||
}
|
||||
v.failures = append(v.failures, lint.Failure{
|
||||
Confidence: 1,
|
||||
Node: v.target.node(ifStmt),
|
||||
Failure: failMsg,
|
||||
})
|
||||
}
|
||||
default:
|
||||
panic("invalid node type for else")
|
||||
}
|
||||
}
|
25
internal/ifelse/target.go
Normal file
25
internal/ifelse/target.go
Normal file
@ -0,0 +1,25 @@
|
||||
package ifelse
|
||||
|
||||
import "go/ast"
|
||||
|
||||
// Target decides what line/column should be indicated by the rule in question.
|
||||
type Target int
|
||||
|
||||
const (
|
||||
// TargetIf means the text refers to the "if"
|
||||
TargetIf Target = iota
|
||||
|
||||
// TargetElse means the text refers to the "else"
|
||||
TargetElse
|
||||
)
|
||||
|
||||
func (t Target) node(ifStmt *ast.IfStmt) ast.Node {
|
||||
switch t {
|
||||
case TargetIf:
|
||||
return ifStmt
|
||||
case TargetElse:
|
||||
return ifStmt.Else
|
||||
default:
|
||||
panic("bad target")
|
||||
}
|
||||
}
|
@ -2,9 +2,8 @@ package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
"github.com/mgechev/revive/internal/ifelse"
|
||||
"github.com/mgechev/revive/lint"
|
||||
)
|
||||
|
||||
@ -13,16 +12,8 @@ import (
|
||||
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
|
||||
func (e *EarlyReturnRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
|
||||
return ifelse.Apply(e, file.AST, ifelse.TargetIf)
|
||||
}
|
||||
|
||||
// Name returns the rule name.
|
||||
@ -30,147 +21,26 @@ 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
|
||||
// CheckIfElse evaluates the rule against an ifelse.Chain.
|
||||
func (e *EarlyReturnRule) CheckIfElse(chain ifelse.Chain) (failMsg string) {
|
||||
if !chain.Else.Deviates() {
|
||||
// this rule only applies if the else-block deviates control flow
|
||||
return
|
||||
}
|
||||
|
||||
if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
|
||||
hasIfInitializer = true
|
||||
if chain.HasPriorNonDeviating && !chain.If.IsEmpty() {
|
||||
// if we de-indent this block then a previous branch
|
||||
// might flow into it, affecting program behaviour
|
||||
return
|
||||
}
|
||||
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")
|
||||
if chain.If.Deviates() {
|
||||
// avoid overlapping with superfluous-else
|
||||
return
|
||||
}
|
||||
|
||||
if chain.If.IsEmpty() {
|
||||
return fmt.Sprintf("if c { } else { %[1]v } can be simplified to if !c { %[1]v }", chain.Else)
|
||||
}
|
||||
return fmt.Sprintf("if c { ... } else { %[1]v } can be simplified to if !c { %[1]v } ...", chain.Else)
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
"github.com/mgechev/revive/internal/ifelse"
|
||||
"github.com/mgechev/revive/lint"
|
||||
)
|
||||
|
||||
@ -11,16 +9,8 @@ import (
|
||||
type IndentErrorFlowRule struct{}
|
||||
|
||||
// Apply applies the rule to given file.
|
||||
func (*IndentErrorFlowRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
|
||||
var failures []lint.Failure
|
||||
|
||||
onFailure := func(failure lint.Failure) {
|
||||
failures = append(failures, failure)
|
||||
}
|
||||
|
||||
w := lintElse{make(map[*ast.IfStmt]bool), onFailure}
|
||||
ast.Walk(w, file.AST)
|
||||
return failures
|
||||
func (e *IndentErrorFlowRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
|
||||
return ifelse.Apply(e, file.AST, ifelse.TargetElse)
|
||||
}
|
||||
|
||||
// Name returns the rule name.
|
||||
@ -28,51 +18,23 @@ func (*IndentErrorFlowRule) Name() string {
|
||||
return "indent-error-flow"
|
||||
}
|
||||
|
||||
type lintElse struct {
|
||||
ignore map[*ast.IfStmt]bool
|
||||
onFailure func(lint.Failure)
|
||||
}
|
||||
// CheckIfElse evaluates the rule against an ifelse.Chain.
|
||||
func (e *IndentErrorFlowRule) CheckIfElse(chain ifelse.Chain) (failMsg string) {
|
||||
if !chain.If.Deviates() {
|
||||
// this rule only applies if the if-block deviates control flow
|
||||
return
|
||||
}
|
||||
|
||||
func (w lintElse) Visit(node ast.Node) ast.Visitor {
|
||||
ifStmt, ok := node.(*ast.IfStmt)
|
||||
if !ok || ifStmt.Else == nil {
|
||||
return w
|
||||
if chain.HasPriorNonDeviating {
|
||||
// if we de-indent the "else" block then a previous branch
|
||||
// might flow into it, affecting program behaviour
|
||||
return
|
||||
}
|
||||
if w.ignore[ifStmt] {
|
||||
if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok {
|
||||
w.ignore[elseif] = true
|
||||
}
|
||||
return w
|
||||
|
||||
if !chain.If.Returns() {
|
||||
// avoid overlapping with superfluous-else
|
||||
return
|
||||
}
|
||||
if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok {
|
||||
w.ignore[elseif] = true
|
||||
return w
|
||||
}
|
||||
if _, ok := ifStmt.Else.(*ast.BlockStmt); !ok {
|
||||
// only care about elses without conditions
|
||||
return w
|
||||
}
|
||||
if len(ifStmt.Body.List) == 0 {
|
||||
return w
|
||||
}
|
||||
shortDecl := false // does the if statement have a ":=" initialization statement?
|
||||
if ifStmt.Init != nil {
|
||||
if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
|
||||
shortDecl = true
|
||||
}
|
||||
}
|
||||
lastStmt := ifStmt.Body.List[len(ifStmt.Body.List)-1]
|
||||
if _, ok := lastStmt.(*ast.ReturnStmt); ok {
|
||||
extra := ""
|
||||
if shortDecl {
|
||||
extra = " (move short variable declaration to its own line if necessary)"
|
||||
}
|
||||
w.onFailure(lint.Failure{
|
||||
Confidence: 1,
|
||||
Node: ifStmt.Else,
|
||||
Category: "indent",
|
||||
Failure: "if block ends with a return statement, so drop this else and outdent its block" + extra,
|
||||
})
|
||||
}
|
||||
return w
|
||||
|
||||
return "if block ends with a return statement, so drop this else and outdent its block"
|
||||
}
|
||||
|
@ -2,9 +2,7 @@ package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
"github.com/mgechev/revive/internal/ifelse"
|
||||
"github.com/mgechev/revive/lint"
|
||||
)
|
||||
|
||||
@ -12,27 +10,8 @@ import (
|
||||
type SuperfluousElseRule struct{}
|
||||
|
||||
// Apply applies the rule to given file.
|
||||
func (*SuperfluousElseRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
|
||||
var failures []lint.Failure
|
||||
onFailure := func(failure lint.Failure) {
|
||||
failures = append(failures, failure)
|
||||
}
|
||||
|
||||
branchingFunctions := map[string]map[string]bool{
|
||||
"os": {"Exit": true},
|
||||
"log": {
|
||||
"Fatal": true,
|
||||
"Fatalf": true,
|
||||
"Fatalln": true,
|
||||
"Panic": true,
|
||||
"Panicf": true,
|
||||
"Panicln": true,
|
||||
},
|
||||
}
|
||||
|
||||
w := lintSuperfluousElse{make(map[*ast.IfStmt]bool), onFailure, branchingFunctions}
|
||||
ast.Walk(w, file.AST)
|
||||
return failures
|
||||
func (e *SuperfluousElseRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
|
||||
return ifelse.Apply(e, file.AST, ifelse.TargetElse)
|
||||
}
|
||||
|
||||
// Name returns the rule name.
|
||||
@ -40,75 +19,23 @@ func (*SuperfluousElseRule) Name() string {
|
||||
return "superfluous-else"
|
||||
}
|
||||
|
||||
type lintSuperfluousElse struct {
|
||||
ignore map[*ast.IfStmt]bool
|
||||
onFailure func(lint.Failure)
|
||||
branchingFunctions map[string]map[string]bool
|
||||
}
|
||||
|
||||
func (w lintSuperfluousElse) Visit(node ast.Node) ast.Visitor {
|
||||
ifStmt, ok := node.(*ast.IfStmt)
|
||||
if !ok || ifStmt.Else == nil {
|
||||
return w
|
||||
}
|
||||
if w.ignore[ifStmt] {
|
||||
if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok {
|
||||
w.ignore[elseif] = true
|
||||
}
|
||||
return w
|
||||
}
|
||||
if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok {
|
||||
w.ignore[elseif] = true
|
||||
return w
|
||||
}
|
||||
if _, ok := ifStmt.Else.(*ast.BlockStmt); !ok {
|
||||
// only care about elses without conditions
|
||||
return w
|
||||
}
|
||||
if len(ifStmt.Body.List) == 0 {
|
||||
return w
|
||||
}
|
||||
shortDecl := false // does the if statement have a ":=" initialization statement?
|
||||
if ifStmt.Init != nil {
|
||||
if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
|
||||
shortDecl = true
|
||||
}
|
||||
}
|
||||
extra := ""
|
||||
if shortDecl {
|
||||
extra = " (move short variable declaration to its own line if necessary)"
|
||||
}
|
||||
|
||||
lastStmt := ifStmt.Body.List[len(ifStmt.Body.List)-1]
|
||||
switch stmt := lastStmt.(type) {
|
||||
case *ast.BranchStmt:
|
||||
tok := stmt.Tok.String()
|
||||
if tok != "fallthrough" {
|
||||
w.onFailure(newFailure(ifStmt.Else, "if block ends with a "+tok+" statement, so drop this else and outdent its block"+extra))
|
||||
}
|
||||
case *ast.ExprStmt:
|
||||
if ce, ok := stmt.X.(*ast.CallExpr); ok { // it's a function call
|
||||
if fc, ok := ce.Fun.(*ast.SelectorExpr); ok {
|
||||
if id, ok := fc.X.(*ast.Ident); ok {
|
||||
fn := fc.Sel.Name
|
||||
pkg := id.Name
|
||||
if w.branchingFunctions[pkg][fn] { // it's a call to a branching function
|
||||
w.onFailure(
|
||||
newFailure(ifStmt.Else, fmt.Sprintf("if block ends with call to %s.%s function, so drop this else and outdent its block%s", pkg, fn, extra)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func newFailure(node ast.Node, msg string) lint.Failure {
|
||||
return lint.Failure{
|
||||
Confidence: 1,
|
||||
Node: node,
|
||||
Category: "indent",
|
||||
Failure: msg,
|
||||
}
|
||||
// CheckIfElse evaluates the rule against an ifelse.Chain.
|
||||
func (e *SuperfluousElseRule) CheckIfElse(chain ifelse.Chain) (failMsg string) {
|
||||
if !chain.If.Deviates() {
|
||||
// this rule only applies if the if-block deviates control flow
|
||||
return
|
||||
}
|
||||
|
||||
if chain.HasPriorNonDeviating {
|
||||
// if we de-indent the "else" block then a previous branch
|
||||
// might flow into it, affecting program behaviour
|
||||
return
|
||||
}
|
||||
|
||||
if chain.If.Returns() {
|
||||
// avoid overlapping with indent-error-flow
|
||||
return
|
||||
}
|
||||
|
||||
return fmt.Sprintf("if block ends with %v, so drop this else and outdent its block", chain.If.LongString())
|
||||
}
|
||||
|
8
testdata/early-return.go
vendored
8
testdata/early-return.go
vendored
@ -2,6 +2,8 @@
|
||||
|
||||
package fixtures
|
||||
|
||||
import "os"
|
||||
|
||||
func earlyRet() bool {
|
||||
if cond { // MATCH /if c { ... } else { ... return } can be simplified to if !c { ... return } .../
|
||||
println()
|
||||
@ -124,4 +126,10 @@ func earlyRet() bool {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
if cond { //MATCH /if c { ... } else { ... os.Exit() } can be simplified to if !c { ... os.Exit() } .../
|
||||
println()
|
||||
} else {
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
9
testdata/golint/indent-error-flow.go
vendored
9
testdata/golint/indent-error-flow.go
vendored
@ -38,3 +38,12 @@ func h(f func() bool, x int) string {
|
||||
}
|
||||
}
|
||||
|
||||
func i() string {
|
||||
if err == author.ErrCourseNotFound {
|
||||
return "not found"
|
||||
} else if err == author.AnotherError {
|
||||
return "something else"
|
||||
} else { // MATCH /if block ends with a return statement, so drop this else and outdent its block/
|
||||
return "okay"
|
||||
}
|
||||
}
|
||||
|
108
testdata/superfluous-else.go
vendored
108
testdata/superfluous-else.go
vendored
@ -4,9 +4,9 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func h(f func() bool) string {
|
||||
@ -74,72 +74,82 @@ func j(f func() bool) string {
|
||||
}
|
||||
|
||||
func fatal1() string {
|
||||
if f() {
|
||||
a := 1
|
||||
log.Fatal("x")
|
||||
} else { // MATCH /if block ends with call to log.Fatal function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
if f() {
|
||||
a := 1
|
||||
log.Fatal("x")
|
||||
} else { // MATCH /if block ends with call to log.Fatal function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
func fatal2() string {
|
||||
if f() {
|
||||
a := 1
|
||||
log.Fatalf("x")
|
||||
} else { // MATCH /if block ends with call to log.Fatalf function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
if f() {
|
||||
a := 1
|
||||
log.Fatalf("x")
|
||||
} else { // MATCH /if block ends with call to log.Fatalf function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
func fatal3() string {
|
||||
if f() {
|
||||
a := 1
|
||||
log.Fatalln("x")
|
||||
} else { // MATCH /if block ends with call to log.Fatalln function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
if f() {
|
||||
a := 1
|
||||
log.Fatalln("x")
|
||||
} else { // MATCH /if block ends with call to log.Fatalln function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
func exit1() string {
|
||||
if f() {
|
||||
a := 1
|
||||
os.Exit(2)
|
||||
} else { // MATCH /if block ends with call to os.Exit function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
if f() {
|
||||
a := 1
|
||||
os.Exit(2)
|
||||
} else { // MATCH /if block ends with call to os.Exit function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
func Panic1() string {
|
||||
if f() {
|
||||
a := 1
|
||||
log.Panic(2)
|
||||
} else { // MATCH /if block ends with call to log.Panic function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
if f() {
|
||||
a := 1
|
||||
log.Panic(2)
|
||||
} else { // MATCH /if block ends with call to log.Panic function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
func Panic2() string {
|
||||
if f() {
|
||||
a := 1
|
||||
log.Panicf(2)
|
||||
} else { // MATCH /if block ends with call to log.Panicf function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
if f() {
|
||||
a := 1
|
||||
log.Panicf(2)
|
||||
} else { // MATCH /if block ends with call to log.Panicf function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
func Panic3() string {
|
||||
if f() {
|
||||
a := 1
|
||||
log.Panicln(2)
|
||||
} else { // MATCH /if block ends with call to log.Panicln function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
if f() {
|
||||
a := 1
|
||||
log.Panicln(2)
|
||||
} else { // MATCH /if block ends with call to log.Panicln function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
func Panic4() string {
|
||||
if f() {
|
||||
a := 1
|
||||
panic(2)
|
||||
} else { // MATCH /if block ends with call to panic function, so drop this else and outdent its block/
|
||||
log.Printf("non-positive")
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
@ -154,4 +164,14 @@ func noreg_19(f func() bool, x int) string {
|
||||
} else {
|
||||
// side effect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MultiBranch() string {
|
||||
if _, ok := f(); ok {
|
||||
continue
|
||||
} else if _, err := get(); err == nil {
|
||||
continue
|
||||
} else { // MATCH /if block ends with a continue statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)/
|
||||
delete(m, x)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user