Skip to content

Commit

Permalink
new checker contains
Browse files Browse the repository at this point in the history
  • Loading branch information
mmorel-35 committed Jun 23, 2024
1 parent cea8c9c commit 83a46c5
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 0 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ https://golangci-lint.run/usage/linters/#testifylint
| [blank-import](#blank-import) |||
| [bool-compare](#bool-compare) |||
| [compares](#compares) |||
| [contains](#contains) |||
| [empty](#empty) |||
| [error-is-as](#error-is-as) || 🤏 |
| [error-nil](#error-nil) |||
Expand Down Expand Up @@ -216,6 +217,24 @@ due to the inappropriate recursive nature of `assert.Equal` (based on

---

### contains

```go
assert.True(t, strings.Contains(s, "abc123"))
assert.False(t, strings.Contains(s, "456"))

assert.Contains(t, s, "abc123")
assert.NotContains(t, s, "456")
```

**Autofix**: true. <br>
**Enabled by default**: true. <br>
**Reason**: More appropriate `testify` API with clearer failure message.

---

### empty

```go
Expand Down
2 changes: 2 additions & 0 deletions analyzer/checkers_factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func Test_newCheckers(t *testing.T) {
checkers.NewLen(),
checkers.NewNegativePositive(),
checkers.NewCompares(),
checkers.NewContains(),
checkers.NewErrorNil(),
checkers.NewNilCompare(),
checkers.NewErrorIsAs(),
Expand All @@ -38,6 +39,7 @@ func Test_newCheckers(t *testing.T) {
checkers.NewLen(),
checkers.NewNegativePositive(),
checkers.NewCompares(),
checkers.NewContains(),
checkers.NewErrorNil(),
checkers.NewNilCompare(),
checkers.NewErrorIsAs(),
Expand Down
46 changes: 46 additions & 0 deletions analyzer/testdata/src/checkers-default/contains/contains_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Code generated by testifylint/internal/testgen. DO NOT EDIT.

package contains

import (
"errors"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestContainsChecker(t *testing.T) {
var (
s = "abc123"
errSentinel = errors.New("user not found")
)

// Invalid.
{
assert.Contains(t, s, "abc123") // want "contains: use assert\\.Contains"
assert.Containsf(t, s, "abc123", "msg with args %d %s", 42, "42") // want "contains: use assert\\.Containsf"
assert.NotContains(t, s, "456") // want "contains: use assert\\.NotContains"
assert.NotContainsf(t, s, "456", "msg with args %d %s", 42, "42") // want "contains: use assert\\.NotContainsf"
}

// Valid.
{
assert.Contains(t, s, "abc123")
assert.Containsf(t, s, "abc123", "msg with args %d %s", 42, "42")
assert.NotContains(t, s, "456")
assert.NotContainsf(t, s, "456", "msg with args %d %s", 42, "42")
}

// Ignored.
{
assert.Contains(t, errSentinel.Error(), "user")
assert.Containsf(t, errSentinel.Error(), "user", "msg with args %d %s", 42, "42")
assert.Equal(t, strings.Contains(s, "abc123"), true)
assert.Equalf(t, strings.Contains(s, "abc123"), true, "msg with args %d %s", 42, "42")
assert.False(t, !strings.Contains(s, "abc123"))
assert.Falsef(t, !strings.Contains(s, "abc123"), "msg with args %d %s", 42, "42")
assert.True(t, !strings.Contains(s, "456"))
assert.Truef(t, !strings.Contains(s, "456"), "msg with args %d %s", 42, "42")
}
}
1 change: 1 addition & 0 deletions internal/checkers/checkers_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var registry = checkersRegistry{
{factory: asCheckerFactory(NewLen), enabledByDefault: true},
{factory: asCheckerFactory(NewNegativePositive), enabledByDefault: true},
{factory: asCheckerFactory(NewCompares), enabledByDefault: true},
{factory: asCheckerFactory(NewContains), enabledByDefault: true},
{factory: asCheckerFactory(NewErrorNil), enabledByDefault: true},
{factory: asCheckerFactory(NewNilCompare), enabledByDefault: true},
{factory: asCheckerFactory(NewErrorIsAs), enabledByDefault: true},
Expand Down
2 changes: 2 additions & 0 deletions internal/checkers/checkers_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func TestAll(t *testing.T) {
"len",
"negative-positive",
"compares",
"contains",
"error-nil",
"nil-compare",
"error-is-as",
Expand Down Expand Up @@ -76,6 +77,7 @@ func TestEnabledByDefault(t *testing.T) {
"len",
"negative-positive",
"compares",
"contains",
"error-nil",
"nil-compare",
"error-is-as",
Expand Down
75 changes: 75 additions & 0 deletions internal/checkers/contains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package checkers

import (
"go/ast"

"golang.org/x/tools/go/analysis"
)

// Contains detects situations like
//
// assert.True(t, strings.Contains(s, "abc123"))
// assert.False(t, strings.Contains(s, "456"))
//
// and requires
//
// assert.Contains(t, s, "abc123")
// assert.NotContains(t, s, "456")
type Contains struct{}

// NewContains constructs Contains checker.
func NewContains() Contains { return Contains{} }
func (Contains) Name() string { return "contains" }

func (checker Contains) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic {
switch call.Fn.NameFTrimmed {
case "True":
if len(call.Args) < 1 {
return nil
}

ce, ok := call.Args[0].(*ast.CallExpr)
if !ok {
return nil
}
if len(ce.Args) != 2 {
return nil
}

if isStringsContainsCall(pass, ce) {
const proposed = "Contains"
return newUseFunctionDiagnostic(checker.Name(), call, proposed,
newSuggestedFuncReplacement(call, proposed, analysis.TextEdit{
Pos: ce.Pos(),
End: ce.End(),
NewText: formatAsCallArgs(pass, ce.Args[0], ce.Args[1]),
}),
)
}

case "False":
if len(call.Args) < 1 {
return nil
}

ce, ok := call.Args[0].(*ast.CallExpr)
if !ok {
return nil
}
if len(ce.Args) != 2 {
return nil
}

if isStringsContainsCall(pass, ce) {
const proposed = "NotContains"
return newUseFunctionDiagnostic(checker.Name(), call, proposed,
newSuggestedFuncReplacement(call, proposed, analysis.TextEdit{
Pos: ce.Pos(),
End: ce.End(),
NewText: formatAsCallArgs(pass, ce.Args[0], ce.Args[1]),
}),
)
}
}
return nil
}
11 changes: 11 additions & 0 deletions internal/checkers/helpers_string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package checkers

import (
"go/ast"

"golang.org/x/tools/go/analysis"
)

func isStringsContainsCall(pass *analysis.Pass, ce *ast.CallExpr) bool {
return isPkgFnCall(pass, ce, "strings", "Contains")
}
105 changes: 105 additions & 0 deletions internal/testgen/gen_contains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package main

import (
"strings"
"text/template"

"github.com/Antonboom/testifylint/internal/checkers"
)

type ContainsTestsGenerator struct{}

func (ContainsTestsGenerator) Checker() checkers.Checker {
return checkers.NewContains()
}

func (g ContainsTestsGenerator) TemplateData() any {
checker := g.Checker().Name()

return struct {
CheckerName CheckerName
InvalidAssertions []Assertion
ValidAssertions []Assertion
IgnoredAssertions []Assertion
}{
CheckerName: CheckerName(checker),
InvalidAssertions: []Assertion{
{
Fn: "True",
Argsf: `strings.Contains(s, "abc123")`,
ReportMsgf: checker + ": use %s.%s",
ProposedFn: "Contains",
ProposedArgsf: "s, \"abc123\"",
}, {
Fn: "False",
Argsf: `strings.Contains(s, "456")`,
ReportMsgf: checker + ": use %s.%s",
ProposedFn: "NotContains",
ProposedArgsf: "s, \"456\"",
},
},
ValidAssertions: []Assertion{
{Fn: "Contains", Argsf: `s, "abc123"`},
{Fn: "NotContains", Argsf: `s, "456"`},
},
IgnoredAssertions: []Assertion{
{Fn: "Contains", Argsf: `errSentinel.Error(), "user"`},
{Fn: "Equal", Argsf: `strings.Contains(s, "abc123"), true`},
{Fn: "False", Argsf: `!strings.Contains(s, "abc123")`},
{Fn: "True", Argsf: `!strings.Contains(s, "456")`},
},
}
}

func (ContainsTestsGenerator) ErroredTemplate() Executor {
return template.Must(template.New("ContainsTestsGenerator.ErroredTemplate").
Funcs(fm).
Parse(constainsTestTmpl))
}

func (ContainsTestsGenerator) GoldenTemplate() Executor {
return template.Must(template.New("ContainsTestsGenerator.GoldenTemplate").
Funcs(fm).
Parse(strings.ReplaceAll(constainsTestTmpl, "NewAssertionExpander", "NewAssertionExpander.AsGolden")))
}

const constainsTestTmpl = header + `
package {{ .CheckerName.AsPkgName }}
import (
"errors"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func {{ .CheckerName.AsTestName }}(t *testing.T) {
var (
s = "abc123"
errSentinel = errors.New("user not found")
)
// Invalid.
{
{{- range $ai, $assrn := $.InvalidAssertions }}
{{ NewAssertionExpander.Expand $assrn "assert" "t" nil }}
{{- end }}
}
// Valid.
{
{{- range $ai, $assrn := $.ValidAssertions }}
{{ NewAssertionExpander.Expand $assrn "assert" "t" nil }}
{{- end }}
}
// Ignored.
{
{{- range $ai, $assrn := $.IgnoredAssertions }}
{{ NewAssertionExpander.Expand $assrn "assert" "t" nil }}
{{- end }}
}
}
`
1 change: 1 addition & 0 deletions internal/testgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var checkerTestsGenerators = []CheckerTestsGenerator{
BlankImportTestsGenerator{},
BoolCompareTestsGenerator{},
ComparesTestsGenerator{},
ContainsTestsGenerator{},
EmptyTestsGenerator{},
ErrorNilTestsGenerator{},
ErrorIsAsTestsGenerator{},
Expand Down

0 comments on commit 83a46c5

Please sign in to comment.