Skip to content

Commit b15f712

Browse files
committed
Add two new rules, to validate the Success and HaveOccurred matchers
* Makes sure the `Succeed()` matcher only accepts a single error value, or a function that recieves a Gomega object as its first parameter, in async assertions. * force usage of `Succeed` for functions, and `HaveOccurred` for values. Added a new command line flag: `--force-succeed=true`, to enable the second rule (the rule is disabled by default).
1 parent 7c3d5e2 commit b15f712

23 files changed

+650
-86
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,28 @@ This will probably happen when using the old format:
249249
Eventually(aFunc, 500 * time.Millisecond /*timeout*/, 10 * time.Second /*polling*/).Should(Succeed())
250250
```
251251

252+
### Correct usage of the `Succeed()` matcher [Bug]
253+
The `Succeed()` matcher only accepts a single error value. this rule validates that.
254+
255+
For example:
256+
```go
257+
Expect(42).To(Succeed())
258+
```
259+
260+
But mostly, we want to avoid using this matcher with functions that return multiple values, even if their last
261+
returned value is an error, because this is not supported:
262+
```go
263+
Expect(os.Open("myFile.txt")).To(Succeed())
264+
```
265+
266+
In async assertions (like `Eventually()`), the `Succeed()` matcher may also been used with functions that accept
267+
a Gomega object as their first parameter, and returns nothing, e.g. this is a valid usage of `Eventually`
268+
```go
269+
Eventually(func(g Gomega){
270+
g.Expect(true).To(BeTrue())
271+
}).WithTimeout(10 * time.Millisecond).WithPolling(time.Millisecond).Should(Succeed())
272+
```
273+
252274
### Avoid Spec Pollution: Don't Initialize Variables in Container Nodes [BUG/STYLE]:
253275
***Note***: Only applied when the `--forbid-spec-pollution=true` flag is set (disabled by default).
254276

@@ -476,6 +498,30 @@ will be changed to:
476498
```go
477499
Eventually(aFunc, time.Second*5, time.Second*polling)
478500
```
501+
502+
### Correct usage of the `Succeed()` and the `HaveOccurred()` matchers
503+
This rule enforces using the `Success()` matcher only for functions, and the `HaveOccurred()` matcher only for error
504+
values.
505+
506+
For example:
507+
```go
508+
Expect(err).To(Succeed())
509+
```
510+
will trigger a warning with a suggestion to replace the mather to
511+
```go
512+
Expect(err).ToNot(HaveOccurred())
513+
```
514+
515+
and vice versa:
516+
```go
517+
Expect(myErrorFunc()).ToNot(HaveOccurred())
518+
```
519+
will trigger a warning with a suggestion to replace the mather to
520+
```go
521+
Expect(myErrorFunc()).To(Succeed())
522+
```
523+
***This rule is disabled by default***. Use the `--force-succeed=true` command line flag to enable it.
524+
479525
## Suppress the linter
480526
### Suppress warning from command line
481527
* Use the `--suppress-len-assertion=true` flag to suppress the wrong length and cap assertions warning

analyzer.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ func NewAnalyzerWithConfig(config *types.Config) *analysis.Analyzer {
2525
// NewAnalyzer returns an Analyzer - the package interface with nogo
2626
func NewAnalyzer() *analysis.Analyzer {
2727
config := &types.Config{
28-
SuppressLen: false,
29-
SuppressNil: false,
30-
SuppressErr: false,
31-
SuppressCompare: false,
32-
ForbidFocus: false,
33-
AllowHaveLen0: false,
34-
ForceExpectTo: false,
28+
SuppressLen: false,
29+
SuppressNil: false,
30+
SuppressErr: false,
31+
SuppressCompare: false,
32+
ForbidFocus: false,
33+
AllowHaveLen0: false,
34+
ForceExpectTo: false,
35+
ForceSucceedForFuncs: false,
3536
}
3637

3738
a := NewAnalyzerWithConfig(config)
@@ -50,6 +51,7 @@ func NewAnalyzer() *analysis.Analyzer {
5051
a.Flags.BoolVar(&ignored, "suppress-focus-container", true, "Suppress warning for ginkgo focus containers like FDescribe, FContext, FWhen or FIt. Deprecated and ignored: use --forbid-focus-container instead")
5152
a.Flags.Var(&config.ForbidFocus, "forbid-focus-container", "trigger a warning for ginkgo focus containers like FDescribe, FContext, FWhen or FIt; default = false.")
5253
a.Flags.Var(&config.ForbidSpecPollution, "forbid-spec-pollution", "trigger a warning for variable assignments in ginkgo containers like Describe, Context and When, instead of in BeforeEach(); default = false.")
54+
a.Flags.Var(&config.ForceSucceedForFuncs, "force-succeed", "force using the Succeed matcher for error functions, and the HaveOccurred matcher for non-function error values")
5355

5456
return a
5557
}

analyzer_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ func TestAllUseCases(t *testing.T) {
9393
testName: "cap",
9494
testData: "a/cap",
9595
},
96+
{
97+
testName: "HaveOccurred matcher tests",
98+
testData: "a/haveoccurred",
99+
},
96100
} {
97101
t.Run(tc.testName, func(tt *testing.T) {
98102
analysistest.Run(tt, analysistest.TestData(), ginkgolinter.NewAnalyzer(), tc.testData)
@@ -169,6 +173,13 @@ func TestFlags(t *testing.T) {
169173
"forbid-focus-container": "true",
170174
},
171175
},
176+
{
177+
testName: "force Succeed/HaveOccurred",
178+
testData: []string{"a/confighaveoccurred"},
179+
flags: map[string]string{
180+
"force-succeed": "true",
181+
},
182+
},
172183
} {
173184
t.Run(tc.testName, func(tt *testing.T) {
174185
analyzer := ginkgolinter.NewAnalyzer()

doc.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ For example:
3030
This will probably happen when using the old format:
3131
Eventually(aFunc, 500 * time.Millisecond, 10 * time.Second).Should(Succeed())
3232
33+
* Success matcher validation: [BUG]
34+
The Success matcher expect that the actual argument will be a single error. In async actual assertions, It also allow
35+
functions with Gomega object as the function first parameter.
36+
For example:
37+
Expect(myInt).To(Succeed())
38+
or
39+
Eventually(func() int { return 42 }).Should(Succeed())
40+
3341
* reject variable assignments in ginkgo containers [Bug/Style]:
3442
For example:
3543
var _ = Describe("description", func(){
@@ -96,4 +104,13 @@ methods.
96104
For example:
97105
Eventually(context.Background(), func() bool { return true }, "1s").Should(BeTrue())
98106
Eventually(context.Background(), func() bool { return true }, time.Second*60, 15).Should(BeTrue())
107+
108+
* Success <=> Eventually usage [Style]
109+
enforce that the Succeed() matcher will be used for error functions, and the HaveOccurred() matcher will
110+
be used for error values.
111+
112+
For example:
113+
Expect(err).ToNot(Succeed())
114+
or
115+
Expect(funcRetError().ToNot(HaveOccurred())
99116
`

internal/expression/actual/actual.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type Actual struct {
1515
Clone *ast.CallExpr
1616
Arg ArgPayload
1717
argType gotypes.Type
18+
isTuple bool
1819
isAsync bool
1920
asyncArg *AsyncArg
2021
actualOffset int
@@ -32,13 +33,16 @@ func New(origExpr, cloneExpr *ast.CallExpr, orig *ast.CallExpr, clone *ast.CallE
3233
}
3334

3435
argType := pass.TypesInfo.TypeOf(orig.Args[actualOffset])
36+
isTuple := false
3537

3638
if tpl, ok := argType.(*gotypes.Tuple); ok {
3739
if tpl.Len() > 0 {
3840
argType = tpl.At(0).Type()
3941
} else {
4042
argType = nil
4143
}
44+
45+
isTuple = tpl.Len() > 1
4246
}
4347

4448
isAsyncExpr := gomegainfo.IsAsyncActualMethod(funcName)
@@ -53,6 +57,7 @@ func New(origExpr, cloneExpr *ast.CallExpr, orig *ast.CallExpr, clone *ast.CallE
5357
Clone: clone,
5458
Arg: arg,
5559
argType: argType,
60+
isTuple: isTuple,
5661
isAsync: isAsyncExpr,
5762
asyncArg: asyncArg,
5863
actualOffset: actualOffset,
@@ -72,6 +77,10 @@ func (a *Actual) IsAsync() bool {
7277
return a.isAsync
7378
}
7479

80+
func (a *Actual) IsTuple() bool {
81+
return a.isTuple
82+
}
83+
7584
func (a *Actual) ArgGOType() gotypes.Type {
7685
return a.argType
7786
}

internal/expression/actual/actualarg.go

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@ import (
44
"go/ast"
55
"go/token"
66
gotypes "go/types"
7-
"strings"
8-
97
"golang.org/x/tools/go/analysis"
108

119
"github.com/nunnatsa/ginkgolinter/internal/expression/value"
1210
"github.com/nunnatsa/ginkgolinter/internal/gomegainfo"
13-
"github.com/nunnatsa/ginkgolinter/internal/interfaces"
1411
"github.com/nunnatsa/ginkgolinter/internal/reverseassertion"
1512
)
1613

@@ -26,44 +23,17 @@ const (
2623
CapComparisonActualArgType
2724
NilComparisonActualArgType
2825
BinaryComparisonActualArgType
26+
FuncSigArgType
2927
ErrFuncActualArgType
28+
GomegaParamArgType
29+
MultiRetsArgType
3030

31-
AsyncInvalidFuncCall
3231
ErrorTypeArgType
3332

3433
EqualZero
3534
GreaterThanZero
36-
37-
LastUnusedDontChange
3835
)
3936

40-
var argTypeString = map[ArgType]string{
41-
UnknownActualArgType: "UnknownActualArgType",
42-
ErrActualArgType: "ErrActualArgType",
43-
LenFuncActualArgType: "LenFuncActualArgType",
44-
CapFuncActualArgType: "CapFuncActualArgType",
45-
ComparisonActualArgType: "ComparisonActualArgType",
46-
LenComparisonActualArgType: "LenComparisonActualArgType",
47-
CapComparisonActualArgType: "CapComparisonActualArgType",
48-
NilComparisonActualArgType: "NilComparisonActualArgType",
49-
BinaryComparisonActualArgType: "BinaryComparisonActualArgType",
50-
ErrFuncActualArgType: "ErrFuncActualArgType",
51-
AsyncInvalidFuncCall: "AsyncInvalidFuncCall",
52-
ErrorTypeArgType: "ErrorTypeArgType",
53-
EqualZero: "EqualZero",
54-
GreaterThanZero: "GreaterThanZero",
55-
}
56-
57-
func (a ArgType) String() string {
58-
var vals []string
59-
for mask := UnknownActualArgType; mask < LastUnusedDontChange; mask <<= 1 {
60-
if a&mask != 0 {
61-
vals = append(vals, argTypeString[mask])
62-
}
63-
}
64-
65-
return strings.Join(vals, "|")
66-
}
6737
func (a ArgType) Is(val ArgType) bool {
6838
return a&val != 0
6939
}
@@ -85,19 +55,15 @@ func getActualArgPayload(origActualExpr, actualExprClone *ast.CallExpr, pass *an
8555

8656
case *ast.BinaryExpr:
8757
arg = parseBinaryExpr(expr, argExprClone.(*ast.BinaryExpr), pass)
88-
}
89-
90-
}
9158

92-
t := pass.TypesInfo.TypeOf(origArgExpr)
93-
if sig, ok := t.(*gotypes.Signature); ok {
94-
if sig.Results().Len() == 1 {
95-
if interfaces.ImplementsError(sig.Results().At(0).Type().Underlying()) {
96-
arg = &ErrFuncArgPayload{}
59+
default:
60+
t := pass.TypesInfo.TypeOf(origArgExpr)
61+
if sig, ok := t.(*gotypes.Signature); ok {
62+
arg = getAsyncFuncArg(sig)
9763
}
9864
}
65+
9966
}
100-
// }
10167

10268
if arg != nil {
10369
return arg, actualOffset
@@ -200,12 +166,6 @@ func (f *FuncCallArgPayload) ArgType() ArgType {
200166
return f.argType
201167
}
202168

203-
type ErrFuncArgPayload struct{}
204-
205-
func (*ErrFuncArgPayload) ArgType() ArgType {
206-
return ErrFuncActualArgType | ErrorTypeArgType
207-
}
208-
209169
type ErrPayload struct {
210170
value.Valuer
211171
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package actual
2+
3+
import (
4+
"github.com/nunnatsa/ginkgolinter/internal/gomegahandler"
5+
"github.com/nunnatsa/ginkgolinter/internal/interfaces"
6+
gotypes "go/types"
7+
)
8+
9+
func getAsyncFuncArg(sig *gotypes.Signature) ArgPayload {
10+
argType := FuncSigArgType
11+
if sig.Results().Len() == 1 {
12+
if interfaces.ImplementsError(sig.Results().At(0).Type().Underlying()) {
13+
argType |= ErrFuncActualArgType | ErrorTypeArgType
14+
}
15+
}
16+
17+
if sig.Params().Len() > 0 {
18+
arg := sig.Params().At(0).Type()
19+
if gomegahandler.IsGomegaType(arg) && sig.Results().Len() == 0 {
20+
argType |= FuncSigArgType | GomegaParamArgType
21+
}
22+
}
23+
24+
if sig.Results().Len() > 1 {
25+
argType |= FuncSigArgType | MultiRetsArgType
26+
}
27+
28+
return &FuncSigArgPayload{argType: argType}
29+
}
30+
31+
type FuncSigArgPayload struct {
32+
argType ArgType
33+
}
34+
35+
func (f FuncSigArgPayload) ArgType() ArgType {
36+
return f.argType
37+
}

internal/expression/expression.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,10 @@ func (e *GomegaExpression) ActualArgTypeIs(other actual.ArgType) bool {
291291
return e.actual.Arg.ArgType().Is(other)
292292
}
293293

294+
func (e *GomegaExpression) IsActualTuple() bool {
295+
return e.actual.IsTuple()
296+
}
297+
294298
// Matcher proxies
295299

296300
func (e *GomegaExpression) GetMatcher() *matcher.Matcher {

internal/expression/matcher/matcher.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const ( // gomega matchers
2424
or = "Or"
2525
withTransform = "WithTransform"
2626
matchError = "MatchError"
27+
haveOccurred = "HaveOccurred"
28+
succeed = "Succeed"
2729
)
2830

2931
type Matcher struct {

0 commit comments

Comments
 (0)