1
0
mirror of https://github.com/mgechev/revive.git synced 2025-07-17 01:12:27 +02:00

Allow whitelist for the context parameter check (#616)

* Allow a whitelist for the context parameter check

This allows users to configure a set of types that may appear before
`context.Context`.

Notably, I think this rule is useful for allowing the `*testing.T` type
to come before `context.Context`, though there may be other uses (such
as putting a tracer before it, etc).

See #605 for a little more context on this.

Fixes #605

* Save a level of indentation in context-as-arg validation

We can unindent if we make the above check more specific

* refactoring taking into account chavacava's review

Co-authored-by: chavacava <salvadorcavadini+github@gmail.com>
This commit is contained in:
Euan Kemp
2021-12-31 17:11:18 -08:00
committed by GitHub
parent 305f6c13d2
commit af953e6189
6 changed files with 124 additions and 36 deletions

View File

@ -187,7 +187,16 @@ _Configuration_: N/A
_Description_: By [convention](https://github.com/golang/go/wiki/CodeReviewComments#contexts), `context.Context` should be the first parameter of a function. This rule spots function declarations that do not follow the convention. _Description_: By [convention](https://github.com/golang/go/wiki/CodeReviewComments#contexts), `context.Context` should be the first parameter of a function. This rule spots function declarations that do not follow the convention.
_Configuration_: N/A _Configuration_:
* `allowTypesBefore` : (string) comma-separated list of types that may be before 'context.Context'
Example:
```toml
[rule.context-as-argument]
arguments = [{allowTypesBefore = "*testing.T,*github.com/user/repo/testing.Harness"}]
```
## context-keys-type ## context-keys-type
@ -253,7 +262,7 @@ _Configuration_: N/A
_Description_: In GO it is idiomatic to minimize nesting statements, a typical example is to avoid if-then-else constructions. This rule spots constructions like _Description_: In GO it is idiomatic to minimize nesting statements, a typical example is to avoid if-then-else constructions. This rule spots constructions like
```go ```go
if cond { if cond {
// do something // do something
} else { } else {
// do other thing // do other thing
return ... return ...
@ -263,7 +272,7 @@ that can be rewritten into more idiomatic:
```go ```go
if ! cond { if ! cond {
// do other thing // do other thing
return ... return ...
} }
// do something // do something
@ -315,8 +324,8 @@ _Description_: Exported function and methods should have comments. This warns on
More information [here](https://github.com/golang/go/wiki/CodeReviewComments#doc-comments) More information [here](https://github.com/golang/go/wiki/CodeReviewComments#doc-comments)
_Configuration_: ([]string) rule flags. _Configuration_: ([]string) rule flags.
Please notice that without configuration, the default behavior of the rule is that of its `golint` counterpart. Please notice that without configuration, the default behavior of the rule is that of its `golint` counterpart.
Available flags are: Available flags are:
* _checkPrivateReceivers_ enables checking public methods of private types * _checkPrivateReceivers_ enables checking public methods of private types
@ -426,8 +435,8 @@ Example:
``` ```
## import-shadowing ## import-shadowing
_Description_: In GO it is possible to declare identifiers (packages, structs, _Description_: In GO it is possible to declare identifiers (packages, structs,
interfaces, parameters, receivers, variables, constants...) that conflict with the interfaces, parameters, receivers, variables, constants...) that conflict with the
name of an imported package. This rule spots identifiers that shadow an import. name of an imported package. This rule spots identifiers that shadow an import.
_Configuration_: N/A _Configuration_: N/A
@ -517,7 +526,7 @@ _Configuration_: N/A
## range-val-address ## range-val-address
_Description_: Range variables in a loop are reused at each iteration. This rule warns when assigning the address of the variable, passing the address to append() or using it in a map. _Description_: Range variables in a loop are reused at each iteration. This rule warns when assigning the address of the variable, passing the address to append() or using it in a map.
_Configuration_: N/A _Configuration_: N/A
@ -535,7 +544,7 @@ Even if possible, redefining these built in names can lead to bugs very difficul
_Configuration_: N/A _Configuration_: N/A
## string-of-int ## string-of-int
_Description_: explicit type conversion `string(i)` where `i` has an integer type other than `rune` might behave not as expected by the developer (e.g. `string(42)` is not `"42"`). This rule spot that kind of suspicious conversions. _Description_: explicit type conversion `string(i)` where `i` has an integer type other than `rune` might behave not as expected by the developer (e.g. `string(42)` is not `"42"`). This rule spot that kind of suspicious conversions.
_Configuration_: N/A _Configuration_: N/A
@ -548,7 +557,7 @@ _Configuration_: Each argument is a slice containing 2-3 strings: a scope, a reg
1. The first string defines a scope. This controls which string literals the regex will apply to, and is defined as a function argument. It must contain at least a function name (`core.WriteError`). Scopes may optionally contain a number specifying which argument in the function to check (`core.WriteError[1]`), as well as a struct field (`core.WriteError[1].Message`, only works for top level fields). Function arguments are counted starting at 0, so `[0]` would refer to the first argument, `[1]` would refer to the second, etc. If no argument number is provided, the first argument will be used (same as `[0]`). 1. The first string defines a scope. This controls which string literals the regex will apply to, and is defined as a function argument. It must contain at least a function name (`core.WriteError`). Scopes may optionally contain a number specifying which argument in the function to check (`core.WriteError[1]`), as well as a struct field (`core.WriteError[1].Message`, only works for top level fields). Function arguments are counted starting at 0, so `[0]` would refer to the first argument, `[1]` would refer to the second, etc. If no argument number is provided, the first argument will be used (same as `[0]`).
2. The second string is a regular expression (beginning and ending with a `/` character), which will be used to check the string literals in the scope. 2. The second string is a regular expression (beginning and ending with a `/` character), which will be used to check the string literals in the scope.
3. The third string (optional) is a message containing the purpose for the regex, which will be used in lint errors. 3. The third string (optional) is a message containing the purpose for the regex, which will be used in lint errors.
@ -663,8 +672,8 @@ _Configuration_: N/A
## useless-break ## useless-break
_Description_: This rule warns on useless `break` statements in case clauses of switch and select statements. GO, unlike other programming languages like C, only executes statements of the selected case while ignoring the subsequent case clauses. _Description_: This rule warns on useless `break` statements in case clauses of switch and select statements. GO, unlike other programming languages like C, only executes statements of the selected case while ignoring the subsequent case clauses.
Therefore, inserting a `break` at the end of a case clause has no effect. Therefore, inserting a `break` at the end of a case clause has no effect.
Because `break` statements are rarely used in case clauses, when switch or select statements are inside a for-loop, the programmer might wrongly assume that a `break` in a case clause will take the control out of the loop. Because `break` statements are rarely used in case clauses, when switch or select statements are inside a for-loop, the programmer might wrongly assume that a `break` in a case clause will take the control out of the loop.
The rule emits a specific warning for such cases. The rule emits a specific warning for such cases.

View File

@ -1,28 +1,34 @@
package rule package rule
import ( import (
"fmt"
"go/ast" "go/ast"
"strings"
"github.com/mgechev/revive/lint" "github.com/mgechev/revive/lint"
) )
// ContextAsArgumentRule lints given else constructs. // ContextAsArgumentRule lints given else constructs.
type ContextAsArgumentRule struct{} type ContextAsArgumentRule struct {
allowTypesLUT map[string]struct{}
}
// Apply applies the rule to given file. // Apply applies the rule to given file.
func (r *ContextAsArgumentRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure { func (r *ContextAsArgumentRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
var failures []lint.Failure
fileAst := file.AST if r.allowTypesLUT == nil {
r.allowTypesLUT = getAllowTypesFromArguments(args)
}
var failures []lint.Failure
walker := lintContextArguments{ walker := lintContextArguments{
file: file, allowTypesLUT: r.allowTypesLUT,
fileAst: fileAst,
onFailure: func(failure lint.Failure) { onFailure: func(failure lint.Failure) {
failures = append(failures, failure) failures = append(failures, failure)
}, },
} }
ast.Walk(walker, fileAst) ast.Walk(walker, file.AST)
return failures return failures
} }
@ -33,9 +39,8 @@ func (r *ContextAsArgumentRule) Name() string {
} }
type lintContextArguments struct { type lintContextArguments struct {
file *lint.File allowTypesLUT map[string]struct{}
fileAst *ast.File onFailure func(lint.Failure)
onFailure func(lint.Failure)
} }
func (w lintContextArguments) Visit(n ast.Node) ast.Visitor { func (w lintContextArguments) Visit(n ast.Node) ast.Visitor {
@ -43,12 +48,15 @@ func (w lintContextArguments) Visit(n ast.Node) ast.Visitor {
if !ok || len(fn.Type.Params.List) <= 1 { if !ok || len(fn.Type.Params.List) <= 1 {
return w return w
} }
fnArgs := fn.Type.Params.List
// A context.Context should be the first parameter of a function. // A context.Context should be the first parameter of a function.
// Flag any that show up after the first. // Flag any that show up after the first.
previousArgIsCtx := isPkgDot(fn.Type.Params.List[0].Type, "context", "Context") isCtxStillAllowed := true
for _, arg := range fn.Type.Params.List[1:] { for _, arg := range fnArgs {
argIsCtx := isPkgDot(arg.Type, "context", "Context") argIsCtx := isPkgDot(arg.Type, "context", "Context")
if argIsCtx && !previousArgIsCtx { if argIsCtx && !isCtxStillAllowed {
w.onFailure(lint.Failure{ w.onFailure(lint.Failure{
Node: arg, Node: arg,
Category: "arg-order", Category: "arg-order",
@ -57,7 +65,41 @@ func (w lintContextArguments) Visit(n ast.Node) ast.Visitor {
}) })
break // only flag one break // only flag one
} }
previousArgIsCtx = argIsCtx
typeName := gofmt(arg.Type)
// a parameter of type context.Context is still allowed if the current arg type is in the LUT
_, isCtxStillAllowed = w.allowTypesLUT[typeName]
} }
return w
return nil // avoid visiting the function body
}
func getAllowTypesFromArguments(args lint.Arguments) map[string]struct{} {
allowTypesBefore := []string{}
if len(args) >= 1 {
argKV, ok := args[0].(map[string]interface{})
if !ok {
panic(fmt.Sprintf("Invalid argument to the context-as-argument rule. Expecting a k,v map, got %T", args[0]))
}
for k, v := range argKV {
switch k {
case "allowTypesBefore":
typesBefore, ok := v.(string)
if !ok {
panic(fmt.Sprintf("Invalid argument to the context-as-argument.allowTypesBefore rule. Expecting a string, got %T", v))
}
allowTypesBefore = append(allowTypesBefore, strings.Split(typesBefore, ",")...)
default:
panic(fmt.Sprintf("Invalid argument to the context-as-argument rule. Unrecognized key %s", k))
}
}
}
result := make(map[string]struct{}, len(allowTypesBefore))
for _, v := range allowTypesBefore {
result[v] = struct{}{}
}
result["context.Context"] = struct{}{} // context.Context is always allowed before another context.Context
return result
} }

View File

@ -92,6 +92,7 @@ func validType(T types.Type) bool {
!strings.Contains(T.String(), "invalid type") // good but not foolproof !strings.Contains(T.String(), "invalid type") // good but not foolproof
} }
// isPkgDot checks if the expression is <pkg>.<name>
func isPkgDot(expr ast.Expr, pkg, name string) bool { func isPkgDot(expr ast.Expr, pkg, name string) bool {
sel, ok := expr.(*ast.SelectorExpr) sel, ok := expr.(*ast.SelectorExpr)
return ok && isIdent(sel.X, pkg) && isIdent(sel.Sel, name) return ok && isIdent(sel.X, pkg) && isIdent(sel.Sel, name)
@ -132,14 +133,6 @@ func pick(n ast.Node, fselect func(n ast.Node) bool, f func(n ast.Node) []ast.No
return result return result
} }
func pickFromExpList(l []ast.Expr, fselect func(n ast.Node) bool, f func(n ast.Node) []ast.Node) []ast.Node {
result := make([]ast.Node, 0)
for _, e := range l {
result = append(result, pick(e, fselect, f)...)
}
return result
}
type picker struct { type picker struct {
fselect func(n ast.Node) bool fselect func(n ast.Node) bool
onSelect func(n ast.Node) onSelect func(n ast.Node)

View File

@ -0,0 +1,19 @@
package test
import (
"testing"
"github.com/mgechev/revive/lint"
"github.com/mgechev/revive/rule"
)
func TestContextAsArgument(t *testing.T) {
testRule(t, "context-as-argument", &rule.ContextAsArgumentRule{}, &lint.RuleConfig{
Arguments: []interface{}{
map[string]interface{}{
"allowTypesBefore": "AllowedBeforeType,AllowedBeforeStruct,*AllowedBeforePtrStruct,*testing.T",
},
},
},
)
}

View File

@ -31,7 +31,6 @@ var rules = []lint.Rule{
&rule.UnexportedReturnRule{}, &rule.UnexportedReturnRule{},
&rule.TimeNamingRule{}, &rule.TimeNamingRule{},
&rule.ContextKeysType{}, &rule.ContextKeysType{},
&rule.ContextAsArgumentRule{},
} }
func TestAll(t *testing.T) { func TestAll(t *testing.T) {

View File

@ -5,8 +5,18 @@ package foo
import ( import (
"context" "context"
"testing"
) )
// AllowedBeforeType is a type that is configured to be allowed before context.Context
type AllowedBeforeType string
// AllowedBeforeStruct is a type that is configured to be allowed before context.Context
type AllowedBeforeStruct struct{}
// AllowedBeforePtrStruct is a type that is configured to be allowed before context.Context
type AllowedBeforePtrStruct struct{}
// A proper context.Context location // A proper context.Context location
func x(ctx context.Context) { // ok func x(ctx context.Context) { // ok
} }
@ -15,6 +25,22 @@ func x(ctx context.Context) { // ok
func x(ctx context.Context, s string) { // ok func x(ctx context.Context, s string) { // ok
} }
// *testing.T is permitted in the linter config for the test
func x(t *testing.T, ctx context.Context) { // ok
}
func x(_ AllowedBeforeType, _ AllowedBeforeType, ctx context.Context) { // ok
}
func x(_, _ AllowedBeforeType, ctx context.Context) { // ok
}
func x(_ *AllowedBeforePtrStruct, ctx context.Context) { // ok
}
func x(_ AllowedBeforePtrStruct, ctx context.Context) { // MATCH /context.Context should be the first parameter of a function/
}
// An invalid context.Context location // An invalid context.Context location
func y(s string, ctx context.Context) { // MATCH /context.Context should be the first parameter of a function/ func y(s string, ctx context.Context) { // MATCH /context.Context should be the first parameter of a function/
} }