Skip to content

Commit e36c534

Browse files
authored
Merge pull request #89 from Antonboom/fixes/require-error-bool-expr
require-error: ignore assertion in bool expr
2 parents 8bf6d9d + 673b9be commit e36c534

File tree

5 files changed

+82
-15
lines changed

5 files changed

+82
-15
lines changed

CONTRIBUTING.md

+4
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ assertion call. For more complex checkers, use the [checkers.AdvancedChecker](./
9595
If the checker turns out to be too “fat”, then you can omit some obviously rare combinations,
9696
especially if they are covered by other checkers. Usually these are expressions in `assert.True/False`.
9797

98+
Remember that [assert.TestingT](https://pkg.go.dev/github.com/stretchr/testify/assert#TestingT) and
99+
[require.TestingT](https://pkg.go.dev/github.com/stretchr/testify/require#TestingT) are different interfaces,
100+
which may be important in some contexts.
101+
98102
### 8) Improve tests from p.4 if necessary
99103

100104
Pay attention to `Assertion` and `NewAssertionExpander`, but keep your tests as small as possible.

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,9 @@ By default `require-error` only checks the `*Error*` assertions, presented above
480480
You can set `--require-error.fn-pattern` flag to limit the checking to certain calls (but still from the list above).
481481
For example, `--require-error.fn-pattern="^(Errorf?|NoErrorf?)$"` will only check `Error`, `Errorf`, `NoError`, and `NoErrorf`.
482482

483-
Also, to minimize the number of false positives, `require-error` ignores:
483+
Also, to minimize false positives, `require-error` ignores:
484484
- assertion in the `if` condition;
485+
- assertion in the bool expression;
485486
- the entire `if-else[-if]` block, if there is an assertion in any `if` condition;
486487
- the last assertion in the block, if there are no methods/functions calls after it;
487488
- assertions in an explicit goroutine;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package requireerrorskiplogic
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"github.com/stretchr/testify/require"
6+
)
7+
8+
func assertRegexpError1(regexp any) assert.ErrorAssertionFunc {
9+
return func(t assert.TestingT, got error, msg ...any) bool {
10+
if h, ok := t.(interface{ Helper() }); ok {
11+
h.Helper()
12+
}
13+
return assert.Error(t, got, msg...) && assert.Regexp(t, regexp, got.Error(), msg...)
14+
}
15+
}
16+
17+
func assertRegexpError2(regexp any) assert.ErrorAssertionFunc {
18+
return func(t assert.TestingT, got error, msg ...any) bool {
19+
if h, ok := t.(interface{ Helper() }); ok {
20+
h.Helper()
21+
}
22+
assert.Error(t, got, msg...) // want "require-error: for error assertions use require"
23+
return assert.Regexp(t, regexp, got.Error(), msg...)
24+
}
25+
}
26+
27+
func assertRegexpError3(regexp any) assert.ErrorAssertionFunc {
28+
return func(t assert.TestingT, got error, msg ...any) bool {
29+
if h, ok := t.(interface{ Helper() }); ok {
30+
h.Helper()
31+
}
32+
return assert.Error(t, got, msg...) &&
33+
(assert.Regexp(t, regexp, got.Error(), msg...) ||
34+
assert.ErrorContains(t, got, "failed to"))
35+
}
36+
}
37+
38+
func requireRegexpError1(regexp any) require.ErrorAssertionFunc {
39+
return func(t require.TestingT, got error, msg ...any) {
40+
if h, ok := t.(interface{ Helper() }); ok {
41+
h.Helper()
42+
}
43+
assert.Error(t, got, msg...) // want "require-error: for error assertions use require"
44+
assert.Regexp(t, regexp, got.Error(), msg...)
45+
}
46+
}
47+
48+
func requireRegexpError2(regexp any) require.ErrorAssertionFunc {
49+
return func(t require.TestingT, got error, msg ...any) {
50+
if h, ok := t.(interface{ Helper() }); ok {
51+
h.Helper()
52+
}
53+
require.Error(t, got, msg...)
54+
assert.Regexp(t, regexp, got.Error(), msg...)
55+
}
56+
}

internal/checkers/call_meta.go

+14-7
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,23 @@ func trimTArg(pass *analysis.Pass, args []ast.Expr) []ast.Expr {
121121
}
122122

123123
func isTestingTPtr(pass *analysis.Pass, arg ast.Expr) bool {
124+
return implementsAssertTestingT(pass, arg) || implementsRequireTestingT(pass, arg)
125+
}
126+
127+
func implementsAssertTestingT(pass *analysis.Pass, e ast.Expr) bool {
124128
assertTestingTObj := analysisutil.ObjectOf(pass.Pkg, testify.AssertPkgPath, "TestingT")
129+
return (assertTestingTObj != nil) && implements(pass, e, assertTestingTObj)
130+
}
131+
132+
func implementsRequireTestingT(pass *analysis.Pass, e ast.Expr) bool {
125133
requireTestingTObj := analysisutil.ObjectOf(pass.Pkg, testify.RequirePkgPath, "TestingT")
134+
return (requireTestingTObj != nil) && implements(pass, e, requireTestingTObj)
135+
}
126136

127-
argType := pass.TypesInfo.TypeOf(arg)
128-
if argType == nil {
137+
func implements(pass *analysis.Pass, e ast.Expr, ifaceObj types.Object) bool {
138+
t := pass.TypesInfo.TypeOf(e)
139+
if t == nil {
129140
return false
130141
}
131-
132-
return ((assertTestingTObj != nil) &&
133-
types.Implements(argType, assertTestingTObj.Type().Underlying().(*types.Interface))) ||
134-
((requireTestingTObj != nil) &&
135-
types.Implements(argType, requireTestingTObj.Type().Underlying().(*types.Interface)))
142+
return types.Implements(t, ifaceObj.Type().Underlying().(*types.Interface))
136143
}

internal/checkers/require_error.go

+6-7
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ func (checker RequireError) Check(pass *analysis.Pass, inspector *inspector.Insp
7474
_, prevPrevIsIfStmt := stack[len(stack)-3].(*ast.IfStmt)
7575
inIfCond := prevIsIfStmt || (prevPrevIsIfStmt && prevIsAssignStmt)
7676

77+
_, inBoolExpr := stack[len(stack)-2].(*ast.BinaryExpr)
78+
7779
callExpr := node.(*ast.CallExpr)
7880
testifyCall := NewCallMeta(pass, callExpr)
7981

@@ -84,6 +86,7 @@ func (checker RequireError) Check(pass *analysis.Pass, inspector *inspector.Insp
8486
parentIf: findNearestNode[*ast.IfStmt](stack),
8587
parentBlock: findNearestNode[*ast.BlockStmt](stack),
8688
inIfCond: inIfCond,
89+
inBoolExpr: inBoolExpr,
8790
inNoErrorSeq: false, // Will be filled in below.
8891
}
8992

@@ -148,13 +151,8 @@ func needToSkipBasedOnContext(
148151
otherCalls []*callMeta,
149152
callsByBlock map[*ast.BlockStmt][]*callMeta,
150153
) bool {
151-
if currCall.inNoErrorSeq {
152-
// Skip `assert.NoError` sequence.
153-
return true
154-
}
155-
156-
if currCall.inIfCond {
157-
// Skip assertions in the "if condition".
154+
switch {
155+
case currCall.inIfCond, currCall.inBoolExpr, currCall.inNoErrorSeq:
158156
return true
159157
}
160158

@@ -309,6 +307,7 @@ type callMeta struct {
309307
parentIf *ast.IfStmt // The nearest `if`, can be equal with rootIf.
310308
parentBlock *ast.BlockStmt
311309
inIfCond bool // True for code like `if assert.ErrorAs(t, err, &target) {`.
310+
inBoolExpr bool // True for code like `assert.Error(t, err) && assert.ErrorContains(t, err, "value")`
312311
inNoErrorSeq bool // True for sequence of `assert.NoError` assertions.
313312
}
314313

0 commit comments

Comments
 (0)