1
0
mirror of https://github.com/mgechev/revive.git synced 2024-11-24 08:32:22 +02:00

adds defer rule

This commit is contained in:
chavacava 2020-05-24 20:49:49 +02:00 committed by Minko Gechev
parent 2976b46f8d
commit ad3100c9ec
8 changed files with 269 additions and 0 deletions

View File

@ -353,6 +353,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a
| [`early-return`](./RULES_DESCRIPTIONS.md#early-return) | n/a | Spots if-then-else statements that can be refactored to simplify code reading | no | no |
| [`unconditional-recursion`](./RULES_DESCRIPTIONS.md#unconditional-recursion) | n/a | Warns on function calls that will lead to (direct) infinite recursion | no | no |
| [`identical-branches`](./RULES_DESCRIPTIONS.md#identical-branches) | n/a | Spots if-then-else statements with identical `then` and `else` branches | no | no |
| [`defer`](./RULES_DESCRIPTIONS.md#defer) | map | Warns on some [defer gotchas](https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1) | no | no |
## Configurable rules

View File

@ -19,6 +19,7 @@ List of all available rules.
- [context-keys-type](#context-keys-type)
- [cyclomatic](#cyclomatic)
- [deep-exit](#deep-exit)
- [defer](#defer)
- [dot-imports](#dot-imports)
- [duplicated-imports](#duplicated-imports)
- [early-return](#early-return)
@ -192,6 +193,26 @@ _Description_: Packages exposing functions that can stop program execution by ex
_Configuration_: N/A
## defer
_Description_: This rule warns on some common mistakes when using `defer` statement. It currently alerts on the following situations:
| name | description |
|------|-------------|
| call-chain| even if deferring call-chains of the form `foo()()` is valid, it does not helps code understanding (only the last call is deferred)|
|loop | deferring inside loops can be misleading (deferred functions are not executed at the end of the loop iteration but of the current function) and it could lead to exhausting the execution stack |
| method-call| deferring a call to a method can lead to subtle bugs if the method does not have a pointer receiver|
| recover | calling `recover` outside a deferred function has no effect|
| return | returning values form a deferred function has no effect|
These gotchas are described [here](https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01)
_Configuration_: by default all warnings are enabled but it is possible selectively enable them through configuration. For example to enable only `call-chain` and `loop`:
```toml
[rule.defer]
arguments=[["call-chain","loop"]]
```
## dot-imports
_Description_: Importing with `.` makes the programs much harder to understand because it is unclear whether names belong to the current package or to an imported package.

View File

@ -87,6 +87,7 @@ var allRules = append([]lint.Rule{
&rule.EarlyReturnRule{},
&rule.UnconditionalRecursionRule{},
&rule.IdenticalBranchesRule{},
&rule.DeferRule{},
}, defaultRules...)
var allFormatters = []lint.Formatter{

137
rule/defer.go Normal file
View File

@ -0,0 +1,137 @@
package rule
import (
"fmt"
"go/ast"
"github.com/mgechev/revive/lint"
)
// DeferRule lints unused params in functions.
type DeferRule struct{}
// Apply applies the rule to given file.
func (r *DeferRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
allow := r.allowFromArgs(arguments)
var failures []lint.Failure
onFailure := func(failure lint.Failure) {
failures = append(failures, failure)
}
w := lintDeferRule{onFailure: onFailure, allow: allow}
ast.Walk(w, file.AST)
return failures
}
// Name returns the rule name.
func (r *DeferRule) Name() string {
return "defer"
}
func (r *DeferRule) allowFromArgs(args lint.Arguments) map[string]bool {
if len(args) < 1 {
allow := map[string]bool{
"loop": true,
"call-chain": true,
"method-call": true,
"return": true,
"recover": true,
}
return allow
}
aa, ok := args[0].([]interface{})
if !ok {
panic(fmt.Sprintf("Invalid argument '%v' for 'defer' rule. Expecting []string, got %T", args[0], args[0]))
}
allow := make(map[string]bool, len(aa))
for _, subcase := range aa {
sc, ok := subcase.(string)
if !ok {
panic(fmt.Sprintf("Invalid argument '%v' for 'defer' rule. Expecting string, got %T", subcase, subcase))
}
allow[sc] = true
}
return allow
}
type lintDeferRule struct {
onFailure func(lint.Failure)
inALoop bool
inADefer bool
inAFuncLit bool
allow map[string]bool
}
func (w lintDeferRule) Visit(node ast.Node) ast.Visitor {
switch n := node.(type) {
case *ast.ForStmt:
w.visitSubtree(n.Body, w.inADefer, true, w.inAFuncLit)
return nil
case *ast.RangeStmt:
w.visitSubtree(n.Body, w.inADefer, true, w.inAFuncLit)
return nil
case *ast.FuncLit:
w.visitSubtree(n.Body, w.inADefer, false, true)
return nil
case *ast.ReturnStmt:
if len(n.Results) != 0 && w.inADefer && w.inAFuncLit {
w.newFailure("return in a defer function has no effect", n, 1.0, "logic", "return")
}
case *ast.CallExpr:
if isIdent(n.Fun, "recover") && !w.inADefer {
// confidence is not 1 because recover can be in a function that is deferred elsewhere
w.newFailure("recover must be called inside a deferred function", n, 0.8, "logic", "recover")
}
case *ast.DeferStmt:
w.visitSubtree(n.Call.Fun, true, false, false)
if w.inALoop {
w.newFailure("prefer not to defer inside loops", n, 1.0, "bad practice", "loop")
}
switch fn := n.Call.Fun.(type) {
case *ast.CallExpr:
w.newFailure("prefer not to defer chains of function calls", fn, 1.0, "bad practice", "call-chain")
case *ast.SelectorExpr:
if id, ok := fn.X.(*ast.Ident); ok {
isMethodCall := id != nil && id.Obj != nil && id.Obj.Kind == ast.Typ
if isMethodCall {
w.newFailure("be careful when deferring calls to methods without pointer receiver", fn, 0.8, "bad practice", "method-call")
}
}
}
return nil
}
return w
}
func (w lintDeferRule) visitSubtree(n ast.Node, inADefer, inALoop, inAFuncLit bool) {
nw := &lintDeferRule{
onFailure: w.onFailure,
inADefer: inADefer,
inALoop: inALoop,
inAFuncLit: inAFuncLit,
allow: w.allow}
ast.Walk(nw, n)
}
func (w lintDeferRule) newFailure(msg string, node ast.Node, confidence float64, cat string, subcase string) {
if !w.allow[subcase] {
return
}
w.onFailure(lint.Failure{
Confidence: confidence,
Node: node,
Category: cat,
Failure: msg,
})
}

25
test/defer_test.go Normal file
View File

@ -0,0 +1,25 @@
package test
import (
"testing"
"github.com/mgechev/revive/lint"
"github.com/mgechev/revive/rule"
)
// Defer rule.
func TestDefer(t *testing.T) {
testRule(t, "defer", &rule.DeferRule{})
}
func TestDeferLoopDisabled(t *testing.T) {
testRule(t, "defer-loop-disabled", &rule.DeferRule{}, &lint.RuleConfig{
Arguments: []interface{}{[]interface{}{"return", "recover", "call-chain", "method-call"}},
})
}
func TestDeferOthersDisabled(t *testing.T) {
testRule(t, "defer-only-loop-enabled", &rule.DeferRule{}, &lint.RuleConfig{
Arguments: []interface{}{[]interface{}{"loop"}},
})
}

28
testdata/defer-loop-disabled.go vendored Normal file
View File

@ -0,0 +1,28 @@
package fixtures
import "errors"
type tt int
func (t tt) m() {}
func deferrer1() {
for {
go func() {
defer println()
}()
defer func() {}()
}
defer tt.m() // MATCH /be careful when deferring calls to methods without pointer receiver/
defer func() error {
return errors.New("error") //MATCH /return in a defer function has no effect/
}()
defer recover()
recover() //MATCH /recover must be called inside a deferred function/
defer deferrer()
}

28
testdata/defer-only-loop-enabled.go vendored Normal file
View File

@ -0,0 +1,28 @@
package fixtures
import "errors"
type tt int
func (t tt) m() {}
func deferrer3() {
for {
go func() {
defer println()
}()
defer func() {}() // MATCH /prefer not to defer inside loops/
}
defer tt.m()
defer func() error {
return errors.New("error")
}()
defer recover()
recover()
defer deferrer()
}

28
testdata/defer.go vendored Normal file
View File

@ -0,0 +1,28 @@
package fixtures
import "errors"
type tt int
func (t tt) m() {}
func deferrer() {
for {
go func() {
defer println()
}()
defer func() {}() // MATCH /prefer not to defer inside loops/
}
defer tt.m() // MATCH /be careful when deferring calls to methods without pointer receiver/
defer func() error {
return errors.New("error") //MATCH /return in a defer function has no effect/
}()
defer recover()
recover() //MATCH /recover must be called inside a deferred function/
defer deferrer()
}