2020-05-24 20:49:49 +02:00
package rule
import (
"fmt"
"go/ast"
"github.com/mgechev/revive/lint"
)
2024-12-01 17:44:41 +02:00
// DeferRule lints gotchas in defer statements.
2021-10-17 20:34:48 +02:00
type DeferRule struct {
allow map [ string ] bool
}
2020-05-24 20:49:49 +02:00
2024-12-13 21:38:46 +01:00
// Configure validates the rule configuration, and configures the rule accordingly.
//
// Configuration implements the [lint.ConfigurableRule] interface.
func ( r * DeferRule ) Configure ( arguments lint . Arguments ) error {
2024-12-11 19:35:58 +01:00
list , err := r . allowFromArgs ( arguments )
if err != nil {
return err
}
r . allow = list
return nil
2022-04-10 09:06:59 +02:00
}
// Apply applies the rule to given file.
2024-12-13 21:38:46 +01:00
func ( r * DeferRule ) Apply ( file * lint . File , _ lint . Arguments ) [ ] lint . Failure {
2020-05-24 20:49:49 +02:00
var failures [ ] lint . Failure
onFailure := func ( failure lint . Failure ) {
failures = append ( failures , failure )
}
2021-10-17 20:34:48 +02:00
w := lintDeferRule { onFailure : onFailure , allow : r . allow }
2020-05-24 20:49:49 +02:00
ast . Walk ( w , file . AST )
return failures
}
// Name returns the rule name.
2022-04-10 11:55:13 +02:00
func ( * DeferRule ) Name ( ) string {
2020-05-24 20:49:49 +02:00
return "defer"
}
2024-12-11 19:35:58 +01:00
func ( * DeferRule ) allowFromArgs ( args lint . Arguments ) ( map [ string ] bool , error ) {
2020-05-24 20:49:49 +02:00
if len ( args ) < 1 {
allow := map [ string ] bool {
2022-07-24 00:34:16 -07:00
"loop" : true ,
"call-chain" : true ,
"method-call" : true ,
"return" : true ,
"recover" : true ,
"immediate-recover" : true ,
2020-05-24 20:49:49 +02:00
}
2024-12-11 19:35:58 +01:00
return allow , nil
2020-05-24 20:49:49 +02:00
}
2023-09-24 08:44:02 +02:00
aa , ok := args [ 0 ] . ( [ ] any )
2020-05-24 20:49:49 +02:00
if ! ok {
2024-12-11 19:35:58 +01:00
return nil , fmt . Errorf ( "invalid argument '%v' for 'defer' rule. Expecting []string, got %T" , args [ 0 ] , args [ 0 ] )
2020-05-24 20:49:49 +02:00
}
allow := make ( map [ string ] bool , len ( aa ) )
for _ , subcase := range aa {
sc , ok := subcase . ( string )
if ! ok {
2024-12-11 19:35:58 +01:00
return nil , fmt . Errorf ( "invalid argument '%v' for 'defer' rule. Expecting string, got %T" , subcase , subcase )
2020-05-24 20:49:49 +02:00
}
allow [ sc ] = true
}
2024-12-11 19:35:58 +01:00
return allow , nil
2020-05-24 20:49:49 +02:00
}
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 {
2025-01-18 13:16:19 +02:00
w . newFailure ( "return in a defer function has no effect" , n , 1.0 , lint . FailureCategoryLogic , "return" )
2020-05-24 20:49:49 +02:00
}
case * ast . CallExpr :
2023-08-18 20:21:42 +02:00
isCallToRecover := isIdent ( n . Fun , "recover" )
switch {
case ! w . inADefer && isCallToRecover :
2022-07-24 00:34:16 -07:00
// func fn() { recover() }
//
2020-05-24 20:49:49 +02:00
// confidence is not 1 because recover can be in a function that is deferred elsewhere
2025-01-18 13:16:19 +02:00
w . newFailure ( "recover must be called inside a deferred function" , n , 0.8 , lint . FailureCategoryLogic , "recover" )
2023-08-18 20:21:42 +02:00
case w . inADefer && ! w . inAFuncLit && isCallToRecover :
2022-07-24 00:34:16 -07:00
// defer helper(recover())
//
// confidence is not truly 1 because this could be in a correctly-deferred func,
// but it is very likely to be a misunderstanding of defer's behavior around arguments.
2025-01-18 13:16:19 +02:00
w . newFailure ( "recover must be called inside a deferred function, this is executing recover immediately" , n , 1 , lint . FailureCategoryLogic , "immediate-recover" )
2020-05-24 20:49:49 +02:00
}
2024-08-21 10:38:34 +02:00
return nil // no need to analyze the arguments of the function call
2020-05-24 20:49:49 +02:00
case * ast . DeferStmt :
2022-07-24 00:34:16 -07:00
if isIdent ( n . Call . Fun , "recover" ) {
// defer recover()
//
// confidence is not truly 1 because this could be in a correctly-deferred func,
// but normally this doesn't suppress a panic, and even if it did it would silently discard the value.
2025-01-18 13:16:19 +02:00
w . newFailure ( "recover must be called inside a deferred function, this is executing recover immediately" , n , 1 , lint . FailureCategoryLogic , "immediate-recover" )
2022-07-24 00:34:16 -07:00
}
2020-05-24 20:49:49 +02:00
w . visitSubtree ( n . Call . Fun , true , false , false )
2022-07-24 00:34:16 -07:00
for _ , a := range n . Call . Args {
2023-08-18 20:21:42 +02:00
switch a . ( type ) {
case * ast . FuncLit :
continue // too hard to analyze deferred calls with func literals args
default :
w . visitSubtree ( a , true , false , false ) // check arguments, they should not contain recover()
}
2022-07-24 00:34:16 -07:00
}
2020-05-24 20:49:49 +02:00
if w . inALoop {
2025-01-18 13:16:19 +02:00
w . newFailure ( "prefer not to defer inside loops" , n , 1.0 , lint . FailureCategoryBadPractice , "loop" )
2020-05-24 20:49:49 +02:00
}
switch fn := n . Call . Fun . ( type ) {
case * ast . CallExpr :
2025-01-18 13:16:19 +02:00
w . newFailure ( "prefer not to defer chains of function calls" , fn , 1.0 , lint . FailureCategoryBadPractice , "call-chain" )
2020-05-24 20:49:49 +02:00
case * ast . SelectorExpr :
if id , ok := fn . X . ( * ast . Ident ) ; ok {
isMethodCall := id != nil && id . Obj != nil && id . Obj . Kind == ast . Typ
if isMethodCall {
2025-01-18 13:16:19 +02:00
w . newFailure ( "be careful when deferring calls to methods without pointer receiver" , fn , 0.8 , lint . FailureCategoryBadPractice , "method-call" )
2020-05-24 20:49:49 +02:00
}
}
}
2023-09-23 10:41:34 +02:00
2020-05-24 20:49:49 +02:00
return nil
}
return w
}
func ( w lintDeferRule ) visitSubtree ( n ast . Node , inADefer , inALoop , inAFuncLit bool ) {
2022-07-24 00:34:16 -07:00
nw := lintDeferRule {
2020-05-24 20:49:49 +02:00
onFailure : w . onFailure ,
inADefer : inADefer ,
inALoop : inALoop ,
inAFuncLit : inAFuncLit ,
2022-03-02 12:54:55 +05:30
allow : w . allow ,
}
2020-05-24 20:49:49 +02:00
ast . Walk ( nw , n )
}
2025-01-18 13:16:19 +02:00
func ( w lintDeferRule ) newFailure ( msg string , node ast . Node , confidence float64 , cat lint . FailureCategory , subcase string ) {
2020-05-24 20:49:49 +02:00
if ! w . allow [ subcase ] {
return
}
w . onFailure ( lint . Failure {
Confidence : confidence ,
Node : node ,
Category : cat ,
Failure : msg ,
} )
}