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:
parent
2976b46f8d
commit
ad3100c9ec
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
137
rule/defer.go
Normal 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
25
test/defer_test.go
Normal 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
28
testdata/defer-loop-disabled.go
vendored
Normal 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
28
testdata/defer-only-loop-enabled.go
vendored
Normal 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
28
testdata/defer.go
vendored
Normal 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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user