Skip to content

Commit af953e6

Browse files
euankchavacava
andauthored
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 <[email protected]>
1 parent 305f6c1 commit af953e6

6 files changed

+124
-36
lines changed

RULES_DESCRIPTIONS.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,16 @@ _Configuration_: N/A
187187

188188
_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.
189189

190-
_Configuration_: N/A
190+
_Configuration_:
191+
192+
* `allowTypesBefore` : (string) comma-separated list of types that may be before 'context.Context'
193+
194+
Example:
195+
196+
```toml
197+
[rule.context-as-argument]
198+
arguments = [{allowTypesBefore = "*testing.T,*github.com/user/repo/testing.Harness"}]
199+
```
191200

192201
## context-keys-type
193202

@@ -253,7 +262,7 @@ _Configuration_: N/A
253262
_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
254263
```go
255264
if cond {
256-
// do something
265+
// do something
257266
} else {
258267
// do other thing
259268
return ...
@@ -263,7 +272,7 @@ that can be rewritten into more idiomatic:
263272
```go
264273
if ! cond {
265274
// do other thing
266-
return ...
275+
return ...
267276
}
268277

269278
// do something
@@ -315,8 +324,8 @@ _Description_: Exported function and methods should have comments. This warns on
315324

316325
More information [here](https://github.com/golang/go/wiki/CodeReviewComments#doc-comments)
317326

318-
_Configuration_: ([]string) rule flags.
319-
Please notice that without configuration, the default behavior of the rule is that of its `golint` counterpart.
327+
_Configuration_: ([]string) rule flags.
328+
Please notice that without configuration, the default behavior of the rule is that of its `golint` counterpart.
320329
Available flags are:
321330

322331
* _checkPrivateReceivers_ enables checking public methods of private types
@@ -426,8 +435,8 @@ Example:
426435
```
427436
## import-shadowing
428437

429-
_Description_: In GO it is possible to declare identifiers (packages, structs,
430-
interfaces, parameters, receivers, variables, constants...) that conflict with the
438+
_Description_: In GO it is possible to declare identifiers (packages, structs,
439+
interfaces, parameters, receivers, variables, constants...) that conflict with the
431440
name of an imported package. This rule spots identifiers that shadow an import.
432441

433442
_Configuration_: N/A
@@ -517,7 +526,7 @@ _Configuration_: N/A
517526

518527
## range-val-address
519528

520-
_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.
529+
_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.
521530

522531
_Configuration_: N/A
523532

@@ -535,7 +544,7 @@ Even if possible, redefining these built in names can lead to bugs very difficul
535544
_Configuration_: N/A
536545

537546
## string-of-int
538-
_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.
547+
_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.
539548

540549
_Configuration_: N/A
541550

@@ -548,7 +557,7 @@ _Configuration_: Each argument is a slice containing 2-3 strings: a scope, a reg
548557

549558
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]`).
550559

551-
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.
560+
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.
552561

553562
3. The third string (optional) is a message containing the purpose for the regex, which will be used in lint errors.
554563

@@ -663,8 +672,8 @@ _Configuration_: N/A
663672

664673
## useless-break
665674

666-
_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.
667-
Therefore, inserting a `break` at the end of a case clause has no effect.
675+
_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.
676+
Therefore, inserting a `break` at the end of a case clause has no effect.
668677

669678
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.
670679
The rule emits a specific warning for such cases.

rule/context-as-argument.go

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
11
package rule
22

33
import (
4+
"fmt"
45
"go/ast"
6+
"strings"
57

68
"github.com/mgechev/revive/lint"
79
)
810

911
// ContextAsArgumentRule lints given else constructs.
10-
type ContextAsArgumentRule struct{}
12+
type ContextAsArgumentRule struct {
13+
allowTypesLUT map[string]struct{}
14+
}
1115

1216
// Apply applies the rule to given file.
13-
func (r *ContextAsArgumentRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
14-
var failures []lint.Failure
17+
func (r *ContextAsArgumentRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
1518

16-
fileAst := file.AST
19+
if r.allowTypesLUT == nil {
20+
r.allowTypesLUT = getAllowTypesFromArguments(args)
21+
}
22+
23+
var failures []lint.Failure
1724
walker := lintContextArguments{
18-
file: file,
19-
fileAst: fileAst,
25+
allowTypesLUT: r.allowTypesLUT,
2026
onFailure: func(failure lint.Failure) {
2127
failures = append(failures, failure)
2228
},
2329
}
2430

25-
ast.Walk(walker, fileAst)
31+
ast.Walk(walker, file.AST)
2632

2733
return failures
2834
}
@@ -33,22 +39,24 @@ func (r *ContextAsArgumentRule) Name() string {
3339
}
3440

3541
type lintContextArguments struct {
36-
file *lint.File
37-
fileAst *ast.File
38-
onFailure func(lint.Failure)
42+
allowTypesLUT map[string]struct{}
43+
onFailure func(lint.Failure)
3944
}
4045

4146
func (w lintContextArguments) Visit(n ast.Node) ast.Visitor {
4247
fn, ok := n.(*ast.FuncDecl)
4348
if !ok || len(fn.Type.Params.List) <= 1 {
4449
return w
4550
}
51+
52+
fnArgs := fn.Type.Params.List
53+
4654
// A context.Context should be the first parameter of a function.
4755
// Flag any that show up after the first.
48-
previousArgIsCtx := isPkgDot(fn.Type.Params.List[0].Type, "context", "Context")
49-
for _, arg := range fn.Type.Params.List[1:] {
56+
isCtxStillAllowed := true
57+
for _, arg := range fnArgs {
5058
argIsCtx := isPkgDot(arg.Type, "context", "Context")
51-
if argIsCtx && !previousArgIsCtx {
59+
if argIsCtx && !isCtxStillAllowed {
5260
w.onFailure(lint.Failure{
5361
Node: arg,
5462
Category: "arg-order",
@@ -57,7 +65,41 @@ func (w lintContextArguments) Visit(n ast.Node) ast.Visitor {
5765
})
5866
break // only flag one
5967
}
60-
previousArgIsCtx = argIsCtx
68+
69+
typeName := gofmt(arg.Type)
70+
// a parameter of type context.Context is still allowed if the current arg type is in the LUT
71+
_, isCtxStillAllowed = w.allowTypesLUT[typeName]
6172
}
62-
return w
73+
74+
return nil // avoid visiting the function body
75+
}
76+
77+
func getAllowTypesFromArguments(args lint.Arguments) map[string]struct{} {
78+
allowTypesBefore := []string{}
79+
if len(args) >= 1 {
80+
argKV, ok := args[0].(map[string]interface{})
81+
if !ok {
82+
panic(fmt.Sprintf("Invalid argument to the context-as-argument rule. Expecting a k,v map, got %T", args[0]))
83+
}
84+
for k, v := range argKV {
85+
switch k {
86+
case "allowTypesBefore":
87+
typesBefore, ok := v.(string)
88+
if !ok {
89+
panic(fmt.Sprintf("Invalid argument to the context-as-argument.allowTypesBefore rule. Expecting a string, got %T", v))
90+
}
91+
allowTypesBefore = append(allowTypesBefore, strings.Split(typesBefore, ",")...)
92+
default:
93+
panic(fmt.Sprintf("Invalid argument to the context-as-argument rule. Unrecognized key %s", k))
94+
}
95+
}
96+
}
97+
98+
result := make(map[string]struct{}, len(allowTypesBefore))
99+
for _, v := range allowTypesBefore {
100+
result[v] = struct{}{}
101+
}
102+
103+
result["context.Context"] = struct{}{} // context.Context is always allowed before another context.Context
104+
return result
63105
}

rule/utils.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func validType(T types.Type) bool {
9292
!strings.Contains(T.String(), "invalid type") // good but not foolproof
9393
}
9494

95+
// isPkgDot checks if the expression is <pkg>.<name>
9596
func isPkgDot(expr ast.Expr, pkg, name string) bool {
9697
sel, ok := expr.(*ast.SelectorExpr)
9798
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
132133
return result
133134
}
134135

135-
func pickFromExpList(l []ast.Expr, fselect func(n ast.Node) bool, f func(n ast.Node) []ast.Node) []ast.Node {
136-
result := make([]ast.Node, 0)
137-
for _, e := range l {
138-
result = append(result, pick(e, fselect, f)...)
139-
}
140-
return result
141-
}
142-
143136
type picker struct {
144137
fselect func(n ast.Node) bool
145138
onSelect func(n ast.Node)

test/context-as-argument_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/mgechev/revive/lint"
7+
"github.com/mgechev/revive/rule"
8+
)
9+
10+
func TestContextAsArgument(t *testing.T) {
11+
testRule(t, "context-as-argument", &rule.ContextAsArgumentRule{}, &lint.RuleConfig{
12+
Arguments: []interface{}{
13+
map[string]interface{}{
14+
"allowTypesBefore": "AllowedBeforeType,AllowedBeforeStruct,*AllowedBeforePtrStruct,*testing.T",
15+
},
16+
},
17+
},
18+
)
19+
}

test/golint_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ var rules = []lint.Rule{
3131
&rule.UnexportedReturnRule{},
3232
&rule.TimeNamingRule{},
3333
&rule.ContextKeysType{},
34-
&rule.ContextAsArgumentRule{},
3534
}
3635

3736
func TestAll(t *testing.T) {

testdata/golint/context-as-argument.go renamed to testdata/context-as-argument.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@ package foo
55

66
import (
77
"context"
8+
"testing"
89
)
910

11+
// AllowedBeforeType is a type that is configured to be allowed before context.Context
12+
type AllowedBeforeType string
13+
14+
// AllowedBeforeStruct is a type that is configured to be allowed before context.Context
15+
type AllowedBeforeStruct struct{}
16+
17+
// AllowedBeforePtrStruct is a type that is configured to be allowed before context.Context
18+
type AllowedBeforePtrStruct struct{}
19+
1020
// A proper context.Context location
1121
func x(ctx context.Context) { // ok
1222
}
@@ -15,6 +25,22 @@ func x(ctx context.Context) { // ok
1525
func x(ctx context.Context, s string) { // ok
1626
}
1727

28+
// *testing.T is permitted in the linter config for the test
29+
func x(t *testing.T, ctx context.Context) { // ok
30+
}
31+
32+
func x(_ AllowedBeforeType, _ AllowedBeforeType, ctx context.Context) { // ok
33+
}
34+
35+
func x(_, _ AllowedBeforeType, ctx context.Context) { // ok
36+
}
37+
38+
func x(_ *AllowedBeforePtrStruct, ctx context.Context) { // ok
39+
}
40+
41+
func x(_ AllowedBeforePtrStruct, ctx context.Context) { // MATCH /context.Context should be the first parameter of a function/
42+
}
43+
1844
// An invalid context.Context location
1945
func y(s string, ctx context.Context) { // MATCH /context.Context should be the first parameter of a function/
2046
}

0 commit comments

Comments
 (0)