From 2ab72b4feef2c97f22f90cfbf9e45a6cfcd08bda Mon Sep 17 00:00:00 2001 From: Taras Madan Date: Tue, 5 Dec 2023 15:10:03 +0100 Subject: vendor: updates --- vendor/github.com/Antonboom/testifylint/LICENSE | 21 ++ .../Antonboom/testifylint/analyzer/analyzer.go | 175 ++++++++++++++++ .../testifylint/analyzer/checkers_factory.go | 48 +++++ .../testifylint/internal/analysisutil/doc.go | 9 + .../testifylint/internal/analysisutil/file.go | 28 +++ .../testifylint/internal/analysisutil/format.go | 34 ++++ .../testifylint/internal/analysisutil/object.go | 34 ++++ .../testifylint/internal/analysisutil/pkg.go | 19 ++ .../testifylint/internal/checkers/bool_compare.go | 223 +++++++++++++++++++++ .../testifylint/internal/checkers/checker.go | 59 ++++++ .../internal/checkers/checkers_registry.go | 101 ++++++++++ .../testifylint/internal/checkers/compares.go | 95 +++++++++ .../testifylint/internal/checkers/diagnostic.go | 60 ++++++ .../testifylint/internal/checkers/empty.go | 172 ++++++++++++++++ .../testifylint/internal/checkers/error_is_as.go | 124 ++++++++++++ .../testifylint/internal/checkers/error_nil.go | 109 ++++++++++ .../internal/checkers/expected_actual.go | 156 ++++++++++++++ .../testifylint/internal/checkers/float_compare.go | 63 ++++++ .../Antonboom/testifylint/internal/checkers/len.go | 86 ++++++++ .../testifylint/internal/checkers/require_error.go | 35 ++++ .../internal/checkers/suite_dont_use_pkg.go | 96 +++++++++ .../internal/checkers/suite_extra_assert_call.go | 99 +++++++++ .../testifylint/internal/checkers/suite_thelper.go | 130 ++++++++++++ .../testifylint/internal/config/config.go | 53 +++++ .../internal/config/flag_value_types.go | 105 ++++++++++ .../testifylint/internal/testify/const.go | 13 ++ 26 files changed, 2147 insertions(+) create mode 100644 vendor/github.com/Antonboom/testifylint/LICENSE create mode 100644 vendor/github.com/Antonboom/testifylint/analyzer/analyzer.go create mode 100644 vendor/github.com/Antonboom/testifylint/analyzer/checkers_factory.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/analysisutil/doc.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/analysisutil/file.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/analysisutil/format.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/analysisutil/object.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/analysisutil/pkg.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/bool_compare.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/checker.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/checkers_registry.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/compares.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/diagnostic.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/empty.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/error_is_as.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/error_nil.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/expected_actual.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/float_compare.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/len.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/require_error.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/suite_dont_use_pkg.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/suite_extra_assert_call.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/checkers/suite_thelper.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/config/config.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/config/flag_value_types.go create mode 100644 vendor/github.com/Antonboom/testifylint/internal/testify/const.go (limited to 'vendor/github.com/Antonboom') diff --git a/vendor/github.com/Antonboom/testifylint/LICENSE b/vendor/github.com/Antonboom/testifylint/LICENSE new file mode 100644 index 000000000..9b1cf3a39 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Anton Telyshev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/Antonboom/testifylint/analyzer/analyzer.go b/vendor/github.com/Antonboom/testifylint/analyzer/analyzer.go new file mode 100644 index 000000000..c0b98f83c --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/analyzer/analyzer.go @@ -0,0 +1,175 @@ +package analyzer + +import ( + "fmt" + "go/ast" + "go/types" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" + + "github.com/Antonboom/testifylint/internal/analysisutil" + "github.com/Antonboom/testifylint/internal/checkers" + "github.com/Antonboom/testifylint/internal/config" + "github.com/Antonboom/testifylint/internal/testify" +) + +const ( + name = "testifylint" + doc = "Checks usage of " + testify.ModulePath + "." + url = "https://github.com/antonboom/" + name +) + +// New returns new instance of testifylint analyzer. +func New() *analysis.Analyzer { + cfg := config.NewDefault() + + analyzer := &analysis.Analyzer{ + Name: name, + Doc: doc, + URL: url, + Run: func(pass *analysis.Pass) (any, error) { + regularCheckers, advancedCheckers, err := newCheckers(cfg) + if err != nil { + return nil, fmt.Errorf("build checkers: %v", err) + } + + tl := &testifyLint{ + regularCheckers: regularCheckers, + advancedCheckers: advancedCheckers, + } + return tl.run(pass) + }, + } + config.BindToFlags(&cfg, &analyzer.Flags) + + return analyzer +} + +type testifyLint struct { + regularCheckers []checkers.RegularChecker + advancedCheckers []checkers.AdvancedChecker +} + +func (tl *testifyLint) run(pass *analysis.Pass) (any, error) { + filesToAnalysis := make([]*ast.File, 0, len(pass.Files)) + for _, f := range pass.Files { + if !analysisutil.Imports(f, testify.AssertPkgPath, testify.RequirePkgPath, testify.SuitePkgPath) { + continue + } + filesToAnalysis = append(filesToAnalysis, f) + } + + insp := inspector.New(filesToAnalysis) + + // Regular checkers. + insp.Preorder([]ast.Node{(*ast.CallExpr)(nil)}, func(node ast.Node) { + tl.regularCheck(pass, node.(*ast.CallExpr)) + }) + + // Advanced checkers. + for _, ch := range tl.advancedCheckers { + for _, d := range ch.Check(pass, insp) { + pass.Report(d) + } + } + + return nil, nil +} + +func (tl *testifyLint) regularCheck(pass *analysis.Pass, ce *ast.CallExpr) { + se, ok := ce.Fun.(*ast.SelectorExpr) + if !ok || se.Sel == nil { + return + } + fnName := se.Sel.Name + + initiatorPkg, isPkgCall := func() (*types.Package, bool) { + // Examples: + // s.Assert -> method of *suite.Suite -> package suite ("vendor/github.com/stretchr/testify/suite") + // s.Assert().Equal -> method of *assert.Assertions -> package assert ("vendor/github.com/stretchr/testify/assert") + // s.Equal -> method of *assert.Assertions -> package assert ("vendor/github.com/stretchr/testify/assert") + // reqObj.Falsef -> method of *require.Assertions -> package require ("vendor/github.com/stretchr/testify/require") + if sel, ok := pass.TypesInfo.Selections[se]; ok { + return sel.Obj().Pkg(), false + } + + // Examples: + // assert.False -> assert -> package assert ("vendor/github.com/stretchr/testify/assert") + // require.NotEqualf -> require -> package require ("vendor/github.com/stretchr/testify/require") + if id, ok := se.X.(*ast.Ident); ok { + if selObj := pass.TypesInfo.ObjectOf(id); selObj != nil { + if pkg, ok := selObj.(*types.PkgName); ok { + return pkg.Imported(), true + } + } + } + return nil, false + }() + if initiatorPkg == nil { + return + } + + isAssert := analysisutil.IsPkg(initiatorPkg, testify.AssertPkgName, testify.AssertPkgPath) + isRequire := analysisutil.IsPkg(initiatorPkg, testify.RequirePkgName, testify.RequirePkgPath) + if !(isAssert || isRequire) { + return + } + + call := &checkers.CallMeta{ + Range: ce, + IsPkg: isPkgCall, + IsAssert: isAssert, + Selector: se, + SelectorXStr: analysisutil.NodeString(pass.Fset, se.X), + Fn: checkers.FnMeta{ + Range: se.Sel, + Name: fnName, + IsFmt: strings.HasSuffix(fnName, "f"), + }, + Args: trimTArg(pass, isAssert, ce.Args), + ArgsRaw: ce.Args, + } + for _, ch := range tl.regularCheckers { + if d := ch.Check(pass, call); d != nil { + pass.Report(*d) + // NOTE(a.telyshev): I'm not interested in multiple diagnostics per assertion. + // This simplifies the code and also makes the linter more efficient. + return + } + } +} + +func trimTArg(pass *analysis.Pass, isAssert bool, args []ast.Expr) []ast.Expr { + if len(args) == 0 { + return args + } + + if isTestingTPtr(pass, isAssert, args[0]) { + return args[1:] + } + return args +} + +func isTestingTPtr(pass *analysis.Pass, isAssert bool, arg ast.Expr) bool { + pkgPath := testify.RequirePkgPath + if isAssert { + pkgPath = testify.AssertPkgPath + } + + testingInterfaceObj := analysisutil.ObjectOf(pass.Pkg, pkgPath, "TestingT") + if testingInterfaceObj == nil { + return false + } + + argType := pass.TypesInfo.TypeOf(arg) + if argType == nil { + return false + } + + return types.Implements( + argType, + testingInterfaceObj.Type().Underlying().(*types.Interface), + ) +} diff --git a/vendor/github.com/Antonboom/testifylint/analyzer/checkers_factory.go b/vendor/github.com/Antonboom/testifylint/analyzer/checkers_factory.go new file mode 100644 index 000000000..e87e35e50 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/analyzer/checkers_factory.go @@ -0,0 +1,48 @@ +package analyzer + +import ( + "fmt" + + "github.com/Antonboom/testifylint/internal/checkers" + "github.com/Antonboom/testifylint/internal/config" +) + +// newCheckers accepts linter config and returns slices of enabled checkers sorted by priority. +func newCheckers(cfg config.Config) ([]checkers.RegularChecker, []checkers.AdvancedChecker, error) { + enabledCheckers := cfg.EnabledCheckers + if len(enabledCheckers) == 0 { + enabledCheckers = checkers.EnabledByDefault() + } + if cfg.EnableAll { + enabledCheckers = checkers.All() + } + + checkers.SortByPriority(enabledCheckers) + + regularCheckers := make([]checkers.RegularChecker, 0, len(enabledCheckers)) + advancedCheckers := make([]checkers.AdvancedChecker, 0, len(enabledCheckers)/2) + + for _, name := range enabledCheckers { + ch, ok := checkers.Get(name) + if !ok { + return nil, nil, fmt.Errorf("unknown checker %q", name) + } + + switch c := ch.(type) { + case *checkers.ExpectedActual: + c.SetExpVarPattern(cfg.ExpectedActual.ExpVarPattern.Regexp) + + case *checkers.SuiteExtraAssertCall: + c.SetMode(cfg.SuiteExtraAssertCall.Mode) + } + + switch casted := ch.(type) { + case checkers.RegularChecker: + regularCheckers = append(regularCheckers, casted) + case checkers.AdvancedChecker: + advancedCheckers = append(advancedCheckers, casted) + } + } + + return regularCheckers, advancedCheckers, nil +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/analysisutil/doc.go b/vendor/github.com/Antonboom/testifylint/internal/analysisutil/doc.go new file mode 100644 index 000000000..b57cbd938 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/analysisutil/doc.go @@ -0,0 +1,9 @@ +// Package analysisutil contains functions common for `analyzer` and `internal/checkers` packages. +// In addition, it is intended to "lighten" these packages. +// +// If the function is common to several packages, or it makes sense to test it separately without +// "polluting" the target package with tests of private functionality, then you can put function in this package. +// +// It's important to avoid dependency on `golang.org/x/tools/go/analysis` in the helpers API. +// This makes the API "narrower" and also allows you to test functions without some "abstraction leaks". +package analysisutil diff --git a/vendor/github.com/Antonboom/testifylint/internal/analysisutil/file.go b/vendor/github.com/Antonboom/testifylint/internal/analysisutil/file.go new file mode 100644 index 000000000..3fc1f42b8 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/analysisutil/file.go @@ -0,0 +1,28 @@ +package analysisutil + +import ( + "go/ast" + "strconv" +) + +// Imports tells if the file imports at least one of the packages. +// If no packages provided then function returns false. +func Imports(file *ast.File, pkgs ...string) bool { + for _, i := range file.Imports { + if i.Path == nil { + continue + } + + path, err := strconv.Unquote(i.Path.Value) + if err != nil { + continue + } + // NOTE(a.telyshev): Don't use `slices.Contains` to keep the minimum module version 1.20. + for _, pkg := range pkgs { // Small O(n). + if pkg == path { + return true + } + } + } + return false +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/analysisutil/format.go b/vendor/github.com/Antonboom/testifylint/internal/analysisutil/format.go new file mode 100644 index 000000000..fcb4b847f --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/analysisutil/format.go @@ -0,0 +1,34 @@ +package analysisutil + +import ( + "bytes" + "go/ast" + "go/format" + "go/token" +) + +// NodeString is a more powerful analogue of types.ExprString. +// Return empty string if node AST is invalid. +func NodeString(fset *token.FileSet, node ast.Node) string { + if v := formatNode(fset, node); v != nil { + return v.String() + } + return "" +} + +// NodeBytes works as NodeString but returns a byte slice. +// Return nil if node AST is invalid. +func NodeBytes(fset *token.FileSet, node ast.Node) []byte { + if v := formatNode(fset, node); v != nil { + return v.Bytes() + } + return nil +} + +func formatNode(fset *token.FileSet, node ast.Node) *bytes.Buffer { + buf := new(bytes.Buffer) + if err := format.Node(buf, fset, node); err != nil { + return nil + } + return buf +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/analysisutil/object.go b/vendor/github.com/Antonboom/testifylint/internal/analysisutil/object.go new file mode 100644 index 000000000..e01fba5c1 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/analysisutil/object.go @@ -0,0 +1,34 @@ +package analysisutil + +import ( + "go/ast" + "go/types" +) + +// ObjectOf works in context of Golang package and returns types.Object for the given object's package and name. +// The search is based on the provided package and its dependencies (imports). +// Returns nil if the object is not found. +func ObjectOf(pkg *types.Package, objPkg, objName string) types.Object { + if pkg.Path() == objPkg { + return pkg.Scope().Lookup(objName) + } + + for _, i := range pkg.Imports() { + if trimVendor(i.Path()) == objPkg { + return i.Scope().Lookup(objName) + } + } + return nil +} + +// IsObj returns true if expression is identifier which notes to given types.Object. +// Useful in combination with types.Universe objects. +func IsObj(typesInfo *types.Info, expr ast.Expr, expected types.Object) bool { + id, ok := expr.(*ast.Ident) + if !ok { + return false + } + + obj := typesInfo.ObjectOf(id) + return obj.Id() == expected.Id() +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/analysisutil/pkg.go b/vendor/github.com/Antonboom/testifylint/internal/analysisutil/pkg.go new file mode 100644 index 000000000..d34be5d34 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/analysisutil/pkg.go @@ -0,0 +1,19 @@ +package analysisutil + +import ( + "go/types" + "strings" +) + +// IsPkg checks that package has corresponding objName and path. +// Supports vendored packages. +func IsPkg(pkg *types.Package, name, path string) bool { + return pkg.Name() == name && trimVendor(pkg.Path()) == path +} + +func trimVendor(path string) string { + if strings.HasPrefix(path, "vendor/") { + return path[len("vendor/"):] + } + return path +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/bool_compare.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/bool_compare.go new file mode 100644 index 000000000..8245ab58e --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/bool_compare.go @@ -0,0 +1,223 @@ +package checkers + +import ( + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/Antonboom/testifylint/internal/analysisutil" +) + +// BoolCompare detects situations like +// +// assert.Equal(t, false, result) +// assert.NotEqual(t, result, true) +// assert.False(t, !result) +// assert.True(t, result == true) +// ... +// +// and requires +// +// assert.False(t, result) +// assert.True(t, result) +type BoolCompare struct{} // + +// NewBoolCompare constructs BoolCompare checker. +func NewBoolCompare() BoolCompare { return BoolCompare{} } +func (BoolCompare) Name() string { return "bool-compare" } + +func (checker BoolCompare) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + newUseFnDiagnostic := func(proposed string, survivingArg ast.Node, replaceStart, replaceEnd token.Pos) *analysis.Diagnostic { + return newUseFunctionDiagnostic(checker.Name(), call, proposed, + newSuggestedFuncReplacement(call, proposed, analysis.TextEdit{ + Pos: replaceStart, + End: replaceEnd, + NewText: analysisutil.NodeBytes(pass.Fset, survivingArg), + }), + ) + } + + newUseTrueDiagnostic := func(survivingArg ast.Node, replaceStart, replaceEnd token.Pos) *analysis.Diagnostic { + return newUseFnDiagnostic("True", survivingArg, replaceStart, replaceEnd) + } + + newUseFalseDiagnostic := func(survivingArg ast.Node, replaceStart, replaceEnd token.Pos) *analysis.Diagnostic { + return newUseFnDiagnostic("False", survivingArg, replaceStart, replaceEnd) + } + + newNeedSimplifyDiagnostic := func(survivingArg ast.Node, replaceStart, replaceEnd token.Pos) *analysis.Diagnostic { + return newDiagnostic(checker.Name(), call, "need to simplify the assertion", + &analysis.SuggestedFix{ + Message: "Simplify the assertion", + TextEdits: []analysis.TextEdit{{ + Pos: replaceStart, + End: replaceEnd, + NewText: analysisutil.NodeBytes(pass.Fset, survivingArg), + }}, + }, + ) + } + + switch call.Fn.Name { + case "Equal", "Equalf": + if len(call.Args) < 2 { + return nil + } + + arg1, arg2 := call.Args[0], call.Args[1] + t1, t2 := isUntypedTrue(pass, arg1), isUntypedTrue(pass, arg2) + f1, f2 := isUntypedFalse(pass, arg1), isUntypedFalse(pass, arg2) + + switch { + case xor(t1, t2): + survivingArg, _ := anyVal([]bool{t1, t2}, arg2, arg1) + return newUseTrueDiagnostic(survivingArg, arg1.Pos(), arg2.End()) + + case xor(f1, f2): + survivingArg, _ := anyVal([]bool{f1, f2}, arg2, arg1) + return newUseFalseDiagnostic(survivingArg, arg1.Pos(), arg2.End()) + } + + case "NotEqual", "NotEqualf": + if len(call.Args) < 2 { + return nil + } + + arg1, arg2 := call.Args[0], call.Args[1] + t1, t2 := isUntypedTrue(pass, arg1), isUntypedTrue(pass, arg2) + f1, f2 := isUntypedFalse(pass, arg1), isUntypedFalse(pass, arg2) + + switch { + case xor(t1, t2): + survivingArg, _ := anyVal([]bool{t1, t2}, arg2, arg1) + return newUseFalseDiagnostic(survivingArg, arg1.Pos(), arg2.End()) + + case xor(f1, f2): + survivingArg, _ := anyVal([]bool{f1, f2}, arg2, arg1) + return newUseTrueDiagnostic(survivingArg, arg1.Pos(), arg2.End()) + } + + case "True", "Truef": + if len(call.Args) < 1 { + return nil + } + expr := call.Args[0] + + { + arg1, ok1 := isComparisonWithTrue(pass, expr, token.EQL) + arg2, ok2 := isComparisonWithFalse(pass, expr, token.NEQ) + + if survivingArg, ok := anyVal([]bool{ok1, ok2}, arg1, arg2); ok { + return newNeedSimplifyDiagnostic(survivingArg, expr.Pos(), expr.End()) + } + } + + { + arg1, ok1 := isComparisonWithTrue(pass, expr, token.NEQ) + arg2, ok2 := isComparisonWithFalse(pass, expr, token.EQL) + arg3, ok3 := isNegation(expr) + + if survivingArg, ok := anyVal([]bool{ok1, ok2, ok3}, arg1, arg2, arg3); ok { + return newUseFalseDiagnostic(survivingArg, expr.Pos(), expr.End()) + } + } + + case "False", "Falsef": + if len(call.Args) < 1 { + return nil + } + expr := call.Args[0] + + { + arg1, ok1 := isComparisonWithTrue(pass, expr, token.EQL) + arg2, ok2 := isComparisonWithFalse(pass, expr, token.NEQ) + + if survivingArg, ok := anyVal([]bool{ok1, ok2}, arg1, arg2); ok { + return newNeedSimplifyDiagnostic(survivingArg, expr.Pos(), expr.End()) + } + } + + { + arg1, ok1 := isComparisonWithTrue(pass, expr, token.NEQ) + arg2, ok2 := isComparisonWithFalse(pass, expr, token.EQL) + arg3, ok3 := isNegation(expr) + + if survivingArg, ok := anyVal([]bool{ok1, ok2, ok3}, arg1, arg2, arg3); ok { + return newUseTrueDiagnostic(survivingArg, expr.Pos(), expr.End()) + } + } + } + return nil +} + +var ( + falseObj = types.Universe.Lookup("false") + trueObj = types.Universe.Lookup("true") +) + +func isUntypedTrue(pass *analysis.Pass, e ast.Expr) bool { + return analysisutil.IsObj(pass.TypesInfo, e, trueObj) +} + +func isUntypedFalse(pass *analysis.Pass, e ast.Expr) bool { + return analysisutil.IsObj(pass.TypesInfo, e, falseObj) +} + +func isComparisonWithTrue(pass *analysis.Pass, e ast.Expr, op token.Token) (ast.Expr, bool) { + return isComparisonWith(pass, e, isUntypedTrue, op) +} + +func isComparisonWithFalse(pass *analysis.Pass, e ast.Expr, op token.Token) (ast.Expr, bool) { + return isComparisonWith(pass, e, isUntypedFalse, op) +} + +type predicate func(pass *analysis.Pass, e ast.Expr) bool + +func isComparisonWith(pass *analysis.Pass, e ast.Expr, predicate predicate, op token.Token) (ast.Expr, bool) { + be, ok := e.(*ast.BinaryExpr) + if !ok { + return nil, false + } + if be.Op != op { + return nil, false + } + + t1, t2 := predicate(pass, be.X), predicate(pass, be.Y) + if xor(t1, t2) { + if t1 { + return be.Y, true + } + return be.X, true + } + return nil, false +} + +func isNegation(e ast.Expr) (ast.Expr, bool) { + ue, ok := e.(*ast.UnaryExpr) + if !ok { + return nil, false + } + return ue.X, ue.Op == token.NOT +} + +func xor(a, b bool) bool { + return a != b +} + +// anyVal returns the first value[i] for which bools[i] is true. +func anyVal[T any](bools []bool, vals ...T) (T, bool) { + if len(bools) != len(vals) { + panic("inconsistent usage of valOr") //nolint:forbidigo // Does not depend on the code being analyzed. + } + + for i, b := range bools { + if b { + return vals[i], true + } + } + + var _default T + return _default, false +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/checker.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/checker.go new file mode 100644 index 000000000..f3249dc3c --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/checker.go @@ -0,0 +1,59 @@ +package checkers + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" +) + +// CallMeta stores meta info about assertion function/method call, for example +// +// assert.Equal(t, 42, result, "helpful comment") +type CallMeta struct { + // Range contains start and end position of assertion call. + analysis.Range + // IsPkg true if this is package (not object) call. + IsPkg bool + // IsAssert true if this is "testify/assert" package (or object) call. + IsAssert bool + // Selector is the AST expression of "assert.Equal". + Selector *ast.SelectorExpr + // SelectorXStr is a string representation of Selector's left part – value before point, e.g. "assert". + SelectorXStr string + // Fn stores meta info about assertion function itself. + Fn FnMeta + // Args stores assertion call arguments but without `t *testing.T` argument. + // E.g [42, result, "helpful comment"]. + Args []ast.Expr + // ArgsRaw stores assertion call initial arguments. + // E.g [t, 42, result, "helpful comment"]. + ArgsRaw []ast.Expr +} + +// FnMeta stores meta info about assertion function itself, for example "Equal". +type FnMeta struct { + // Range contains start and end position of function Name. + analysis.Range + // Name is a function name. + Name string + // IsFmt is true if function is formatted, e.g. "Equalf". + IsFmt bool +} + +// Checker describes named checker. +type Checker interface { + Name() string +} + +// RegularChecker check assertion call presented in CallMeta form. +type RegularChecker interface { + Checker + Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic +} + +// AdvancedChecker implements complex Check logic different from trivial CallMeta check. +type AdvancedChecker interface { + Checker + Check(pass *analysis.Pass, inspector *inspector.Inspector) []analysis.Diagnostic +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/checkers_registry.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/checkers_registry.go new file mode 100644 index 000000000..47eaafb76 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/checkers_registry.go @@ -0,0 +1,101 @@ +package checkers + +import ( + "sort" +) + +// registry stores checkers meta information in checkers' priority order. +var registry = checkersRegistry{ + // Regular checkers. + {factory: asCheckerFactory(NewFloatCompare), enabledByDefault: true}, + {factory: asCheckerFactory(NewBoolCompare), enabledByDefault: true}, + {factory: asCheckerFactory(NewEmpty), enabledByDefault: true}, + {factory: asCheckerFactory(NewLen), enabledByDefault: true}, + {factory: asCheckerFactory(NewCompares), enabledByDefault: true}, + {factory: asCheckerFactory(NewErrorNil), enabledByDefault: true}, + {factory: asCheckerFactory(NewErrorIsAs), enabledByDefault: true}, + {factory: asCheckerFactory(NewRequireError), enabledByDefault: true}, + {factory: asCheckerFactory(NewExpectedActual), enabledByDefault: true}, + {factory: asCheckerFactory(NewSuiteExtraAssertCall), enabledByDefault: true}, + {factory: asCheckerFactory(NewSuiteDontUsePkg), enabledByDefault: true}, + // Advanced checkers. + {factory: asCheckerFactory(NewSuiteTHelper), enabledByDefault: false}, +} + +type checkersRegistry []checkerMeta + +type checkerMeta struct { + factory checkerFactory + enabledByDefault bool +} + +type checkerFactory func() Checker + +func asCheckerFactory[T Checker](fn func() T) checkerFactory { + return func() Checker { + return fn() + } +} + +func (r checkersRegistry) get(name string) (m checkerMeta, priority int, found bool) { + for i, meta := range r { + if meta.factory().Name() == name { + return meta, i, true + } + } + return checkerMeta{}, 0, false +} + +// All returns all checkers names sorted by checker's priority. +func All() []string { + result := make([]string, 0, len(registry)) + for _, meta := range registry { + result = append(result, meta.factory().Name()) + } + return result +} + +// EnabledByDefault returns checkers enabled by default sorted by checker's priority. +func EnabledByDefault() []string { + result := make([]string, 0, len(registry)) + for _, meta := range registry { + if meta.enabledByDefault { + result = append(result, meta.factory().Name()) + } + } + return result +} + +// Get returns new checker instance by checker's name. +func Get(name string) (Checker, bool) { + meta, _, ok := registry.get(name) + if ok { + return meta.factory(), true + } + return nil, false +} + +// IsKnown checks if there is a checker with that name. +func IsKnown(name string) bool { + _, _, ok := registry.get(name) + return ok +} + +// IsEnabledByDefault returns true if a checker is enabled by default. +// Returns false if there is no such checker in the registry. +// For pre-validation use Get or IsKnown. +func IsEnabledByDefault(name string) bool { + meta, _, ok := registry.get(name) + return ok && meta.enabledByDefault +} + +// SortByPriority mutates the input checkers names by sorting them in checker priority order. +// Ignores unknown checkers. For pre-validation use Get or IsKnown. +func SortByPriority(checkers []string) { + sort.Slice(checkers, func(i, j int) bool { + lhs, rhs := checkers[i], checkers[j] + _, lhsPriority, _ := registry.get(lhs) + _, rhsPriority, _ := registry.get(rhs) + return lhsPriority < rhsPriority + }) +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/compares.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/compares.go new file mode 100644 index 000000000..afc829f97 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/compares.go @@ -0,0 +1,95 @@ +package checkers + +import ( + "bytes" + "go/ast" + "go/token" + + "golang.org/x/tools/go/analysis" + + "github.com/Antonboom/testifylint/internal/analysisutil" +) + +// Compares detects situations like +// +// assert.True(t, a == b) +// assert.True(t, a != b) +// assert.True(t, a > b) +// assert.True(t, a >= b) +// assert.True(t, a < b) +// assert.True(t, a <= b) +// ... +// +// and requires +// +// assert.Equal(t, a, b) +// assert.NotEqual(t, a, b) +// assert.Greater(t, a, b) +// assert.GreaterOrEqual(t, a, b) +// assert.Less(t, a, b) +// assert.LessOrEqual(t, a, b) +type Compares struct{} + +// NewCompares constructs Compares checker. +func NewCompares() Compares { return Compares{} } +func (Compares) Name() string { return "compares" } + +func (checker Compares) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + if len(call.Args) < 1 { + return nil + } + + be, ok := call.Args[0].(*ast.BinaryExpr) + if !ok { + return nil + } + + var tokenToProposedFn map[token.Token]string + + switch call.Fn.Name { + case "True", "Truef": + tokenToProposedFn = tokenToProposedFnInsteadOfTrue + case "False", "Falsef": + tokenToProposedFn = tokenToProposedFnInsteadOfFalse + default: + return nil + } + + if proposedFn, ok := tokenToProposedFn[be.Op]; ok { + a, b := be.X, be.Y + return newUseFunctionDiagnostic(checker.Name(), call, proposedFn, + newSuggestedFuncReplacement(call, proposedFn, analysis.TextEdit{ + Pos: be.X.Pos(), + End: be.Y.End(), + NewText: formatAsCallArgs(pass, a, b), + }), + ) + } + return nil +} + +var tokenToProposedFnInsteadOfTrue = map[token.Token]string{ + token.EQL: "Equal", + token.NEQ: "NotEqual", + token.GTR: "Greater", + token.GEQ: "GreaterOrEqual", + token.LSS: "Less", + token.LEQ: "LessOrEqual", +} + +var tokenToProposedFnInsteadOfFalse = map[token.Token]string{ + token.EQL: "NotEqual", + token.NEQ: "Equal", + token.GTR: "LessOrEqual", + token.GEQ: "Less", + token.LSS: "GreaterOrEqual", + token.LEQ: "Greater", +} + +// formatAsCallArgs joins a and b and return bytes like `a, b`. +func formatAsCallArgs(pass *analysis.Pass, a, b ast.Node) []byte { + return bytes.Join([][]byte{ + analysisutil.NodeBytes(pass.Fset, a), + analysisutil.NodeBytes(pass.Fset, b), + }, []byte(", ")) +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/diagnostic.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/diagnostic.go new file mode 100644 index 000000000..4ab69c69b --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/diagnostic.go @@ -0,0 +1,60 @@ +package checkers + +import ( + "fmt" + + "golang.org/x/tools/go/analysis" +) + +func newUseFunctionDiagnostic( + checker string, + call *CallMeta, + proposedFn string, + fix *analysis.SuggestedFix, +) *analysis.Diagnostic { + f := proposedFn + if call.Fn.IsFmt { + f += "f" + } + msg := fmt.Sprintf("use %s.%s", call.SelectorXStr, f) + + return newDiagnostic(checker, call, msg, fix) +} + +func newDiagnostic( + checker string, + rng analysis.Range, + msg string, + fix *analysis.SuggestedFix, +) *analysis.Diagnostic { + d := analysis.Diagnostic{ + Pos: rng.Pos(), + End: rng.End(), + Category: checker, + Message: checker + ": " + msg, + } + if fix != nil { + d.SuggestedFixes = []analysis.SuggestedFix{*fix} + } + return &d +} + +func newSuggestedFuncReplacement( + call *CallMeta, + proposedFn string, + additionalEdits ...analysis.TextEdit, +) *analysis.SuggestedFix { + if call.Fn.IsFmt { + proposedFn += "f" + } + return &analysis.SuggestedFix{ + Message: fmt.Sprintf("Replace `%s` with `%s`", call.Fn.Name, proposedFn), + TextEdits: append([]analysis.TextEdit{ + { + Pos: call.Fn.Pos(), + End: call.Fn.End(), + NewText: []byte(proposedFn), + }, + }, additionalEdits...), + } +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/empty.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/empty.go new file mode 100644 index 000000000..79c64205f --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/empty.go @@ -0,0 +1,172 @@ +package checkers + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/Antonboom/testifylint/internal/analysisutil" +) + +// Empty detects situations like +// +// assert.Len(t, arr, 0) +// assert.Equal(t, 0, len(arr)) +// assert.NotEqual(t, 0, len(arr)) +// assert.GreaterOrEqual(t, len(arr), 1) +// ... +// +// and requires +// +// assert.Empty(t, arr) +// assert.NotEmpty(t, arr) +type Empty struct{} + +// NewEmpty constructs Empty checker. +func NewEmpty() Empty { return Empty{} } +func (Empty) Name() string { return "empty" } + +func (checker Empty) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + if d := checker.checkEmpty(pass, call); d != nil { + return d + } + return checker.checkNotEmpty(pass, call) +} + +func (checker Empty) checkEmpty(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { //nolint:gocognit + newUseEmptyDiagnostic := func(replaceStart, replaceEnd token.Pos, replaceWith ast.Expr) *analysis.Diagnostic { + const proposed = "Empty" + return newUseFunctionDiagnostic(checker.Name(), call, proposed, + newSuggestedFuncReplacement(call, proposed, analysis.TextEdit{ + Pos: replaceStart, + End: replaceEnd, + NewText: analysisutil.NodeBytes(pass.Fset, replaceWith), + }), + ) + } + + if len(call.Args) < 2 { + return nil + } + a, b := call.Args[0], call.Args[1] + + switch call.Fn.Name { + case "Len", "Lenf": + if isZero(b) { + return newUseEmptyDiagnostic(a.Pos(), b.End(), a) + } + + case "Equal", "Equalf": + arg1, ok1 := isLenCallAndZero(pass, a, b) + arg2, ok2 := isLenCallAndZero(pass, b, a) + + if lenArg, ok := anyVal([]bool{ok1, ok2}, arg1, arg2); ok { + return newUseEmptyDiagnostic(a.Pos(), b.End(), lenArg) + } + + case "LessOrEqual", "LessOrEqualf": + if lenArg, ok := isBuiltinLenCall(pass, a); ok && isZero(b) { + return newUseEmptyDiagnostic(a.Pos(), b.End(), lenArg) + } + + case "GreaterOrEqual", "GreaterOrEqualf": + if lenArg, ok := isBuiltinLenCall(pass, b); ok && isZero(a) { + return newUseEmptyDiagnostic(a.Pos(), b.End(), lenArg) + } + + case "Less", "Lessf": + if lenArg, ok := isBuiltinLenCall(pass, a); ok && isOne(b) { + return newUseEmptyDiagnostic(a.Pos(), b.End(), lenArg) + } + + case "Greater", "Greaterf": + if lenArg, ok := isBuiltinLenCall(pass, b); ok && isOne(a) { + return newUseEmptyDiagnostic(a.Pos(), b.End(), lenArg) + } + } + return nil +} + +func (checker Empty) checkNotEmpty(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { //nolint:gocognit + newUseNotEmptyDiagnostic := func(replaceStart, replaceEnd token.Pos, replaceWith ast.Expr) *analysis.Diagnostic { + const proposed = "NotEmpty" + return newUseFunctionDiagnostic(checker.Name(), call, proposed, + newSuggestedFuncReplacement(call, proposed, analysis.TextEdit{ + Pos: replaceStart, + End: replaceEnd, + NewText: analysisutil.NodeBytes(pass.Fset, replaceWith), + }), + ) + } + + if len(call.Args) < 2 { + return nil + } + a, b := call.Args[0], call.Args[1] + + switch call.Fn.Name { + case "NotEqual", "NotEqualf": + arg1, ok1 := isLenCallAndZero(pass, a, b) + arg2, ok2 := isLenCallAndZero(pass, b, a) + + if lenArg, ok := anyVal([]bool{ok1, ok2}, arg1, arg2); ok { + return newUseNotEmptyDiagnostic(a.Pos(), b.End(), lenArg) + } + + case "Greater", "Greaterf": + if lenArg, ok := isBuiltinLenCall(pass, a); ok && isZero(b) || isOne(b) { + return newUseNotEmptyDiagnostic(a.Pos(), b.End(), lenArg) + } + + case "Less", "Lessf": + if lenArg, ok := isBuiltinLenCall(pass, b); ok && isZero(a) || isOne(a) { + return newUseNotEmptyDiagnostic(a.Pos(), b.End(), lenArg) + } + + case "GreaterOrEqual", "GreaterOrEqualf": + if lenArg, ok := isBuiltinLenCall(pass, a); ok && isOne(b) { + return newUseNotEmptyDiagnostic(a.Pos(), b.End(), lenArg) + } + + case "LessOrEqual", "LessOrEqualf": + if lenArg, ok := isBuiltinLenCall(pass, b); ok && isOne(a) { + return newUseNotEmptyDiagnostic(a.Pos(), b.End(), lenArg) + } + } + return nil +} + +var lenObj = types.Universe.Lookup("len") + +func isLenCallAndZero(pass *analysis.Pass, a, b ast.Expr) (ast.Expr, bool) { + lenArg, ok := isBuiltinLenCall(pass, a) + return lenArg, ok && isZero(b) +} + +func isBuiltinLenCall(pass *analysis.Pass, e ast.Expr) (ast.Expr, bool) { + ce, ok := e.(*ast.CallExpr) + if !ok { + return nil, false + } + + if analysisutil.IsObj(pass.TypesInfo, ce.Fun, lenObj) && len(ce.Args) == 1 { + return ce.Args[0], true + } + return nil, false +} + +func isZero(e ast.Expr) bool { + return isIntNumber(e, 0) +} + +func isOne(e ast.Expr) bool { + return isIntNumber(e, 1) +} + +func isIntNumber(e ast.Expr, v int) bool { + bl, ok := e.(*ast.BasicLit) + return ok && bl.Kind == token.INT && bl.Value == fmt.Sprintf("%d", v) +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/error_is_as.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/error_is_as.go new file mode 100644 index 000000000..e6abd0ba4 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/error_is_as.go @@ -0,0 +1,124 @@ +package checkers + +import ( + "fmt" + "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/Antonboom/testifylint/internal/analysisutil" +) + +// ErrorIsAs detects situations like +// +// assert.Error(t, err, errSentinel) +// assert.NoError(t, err, errSentinel) +// assert.True(t, errors.Is(err, errSentinel)) +// assert.False(t, errors.Is(err, errSentinel)) +// assert.True(t, errors.As(err, &target)) +// +// and requires +// +// assert.ErrorIs(t, err, errSentinel) +// assert.NotErrorIs(t, err, errSentinel) +// assert.ErrorAs(t, err, &target) +type ErrorIsAs struct{} + +// NewErrorIsAs constructs ErrorIsAs checker. +func NewErrorIsAs() ErrorIsAs { return ErrorIsAs{} } +func (ErrorIsAs) Name() string { return "error-is-as" } + +func (checker ErrorIsAs) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + switch call.Fn.Name { + case "Error", "Errorf": + if len(call.Args) >= 2 && isError(pass, call.Args[1]) { + const proposed = "ErrorIs" + msg := fmt.Sprintf("invalid usage of %[1]s.Error, use %[1]s.%[2]s instead", call.SelectorXStr, proposed) + return newDiagnostic(checker.Name(), call, msg, newSuggestedFuncReplacement(call, proposed)) + } + + case "NoError", "NoErrorf": + if len(call.Args) >= 2 && isError(pass, call.Args[1]) { + const proposed = "NotErrorIs" + msg := fmt.Sprintf("invalid usage of %[1]s.NoError, use %[1]s.%[2]s instead", call.SelectorXStr, proposed) + return newDiagnostic(checker.Name(), call, msg, newSuggestedFuncReplacement(call, proposed)) + } + + case "True", "Truef": + 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 + } + + var proposed string + switch { + case isErrorsIsCall(pass, ce): + proposed = "ErrorIs" + case isErrorsAsCall(pass, ce): + proposed = "ErrorAs" + } + if proposed != "" { + 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", "Falsef": + 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 isErrorsIsCall(pass, ce) { + const proposed = "NotErrorIs" + 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 +} + +func isErrorsIsCall(pass *analysis.Pass, ce *ast.CallExpr) bool { + return isErrorsPkgFnCall(pass, ce, "Is") +} + +func isErrorsAsCall(pass *analysis.Pass, ce *ast.CallExpr) bool { + return isErrorsPkgFnCall(pass, ce, "As") +} + +func isErrorsPkgFnCall(pass *analysis.Pass, ce *ast.CallExpr, fn string) bool { + se, ok := ce.Fun.(*ast.SelectorExpr) + if !ok { + return false + } + + errorsIsObj := analysisutil.ObjectOf(pass.Pkg, "errors", fn) + if errorsIsObj == nil { + return false + } + + return analysisutil.IsObj(pass.TypesInfo, se.Sel, errorsIsObj) +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/error_nil.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/error_nil.go new file mode 100644 index 000000000..b45629a48 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/error_nil.go @@ -0,0 +1,109 @@ +package checkers + +import ( + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/Antonboom/testifylint/internal/analysisutil" +) + +// ErrorNil detects situations like +// +// assert.Nil(t, err) +// assert.NotNil(t, err) +// assert.Equal(t, err, nil) +// assert.NotEqual(t, err, nil) +// +// and requires +// +// assert.NoError(t, err) +// assert.Error(t, err) +type ErrorNil struct{} + +// NewErrorNil constructs ErrorNil checker. +func NewErrorNil() ErrorNil { return ErrorNil{} } +func (ErrorNil) Name() string { return "error-nil" } + +func (checker ErrorNil) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + const ( + errorFn = "Error" + noErrorFn = "NoError" + ) + + proposedFn, survivingArg, replacementEndPos := func() (string, ast.Expr, token.Pos) { + switch call.Fn.Name { + case "NotNil", "NotNilf": + if len(call.Args) >= 1 && isError(pass, call.Args[0]) { + return errorFn, call.Args[0], call.Args[0].End() + } + + case "Nil", "Nilf": + if len(call.Args) >= 1 && isError(pass, call.Args[0]) { + return noErrorFn, call.Args[0], call.Args[0].End() + } + + case "Equal", "Equalf": + if len(call.Args) < 2 { + return "", nil, token.NoPos + } + a, b := call.Args[0], call.Args[1] + + switch { + case isError(pass, a) && isNil(pass, b): + return noErrorFn, a, b.End() + case isNil(pass, a) && isError(pass, b): + return noErrorFn, b, b.End() + } + + case "NotEqual", "NotEqualf": + if len(call.Args) < 2 { + return "", nil, token.NoPos + } + a, b := call.Args[0], call.Args[1] + + switch { + case isError(pass, a) && isNil(pass, b): + return errorFn, a, b.End() + case isNil(pass, a) && isError(pass, b): + return errorFn, b, b.End() + } + } + return "", nil, token.NoPos + }() + + if proposedFn != "" { + return newUseFunctionDiagnostic(checker.Name(), call, proposedFn, + newSuggestedFuncReplacement(call, proposedFn, analysis.TextEdit{ + Pos: call.Args[0].Pos(), + End: replacementEndPos, + NewText: analysisutil.NodeBytes(pass.Fset, survivingArg), + }), + ) + } + return nil +} + +var errIface = types.Universe.Lookup("error").Type().Underlying().(*types.Interface) + +func isError(pass *analysis.Pass, expr ast.Expr) bool { + t := pass.TypesInfo.TypeOf(expr) + if t == nil { + return false + } + + _, ok := t.Underlying().(*types.Interface) + return ok && types.Implements(t, errIface) +} + +func isNil(pass *analysis.Pass, expr ast.Expr) bool { + t := pass.TypesInfo.TypeOf(expr) + if t == nil { + return false + } + + b, ok := t.(*types.Basic) + return ok && b.Kind()&types.UntypedNil > 0 +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/expected_actual.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/expected_actual.go new file mode 100644 index 000000000..ff8243980 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/expected_actual.go @@ -0,0 +1,156 @@ +package checkers + +import ( + "go/ast" + "go/types" + "regexp" + + "golang.org/x/tools/go/analysis" +) + +// DefaultExpectedVarPattern matches variables with "expected" or "wanted" prefix or suffix in the name. +var DefaultExpectedVarPattern = regexp.MustCompile( + `(^(exp(ected)?|want(ed)?)([A-Z]\w*)?$)|(^(\w*[a-z])?(Exp(ected)?|Want(ed)?)$)`) + +// ExpectedActual detects situation like +// +// assert.NotEqual(t, result, "expected value") +// +// and requires +// +// assert.NotEqual(t, "expected value", result) +type ExpectedActual struct { + expVarPattern *regexp.Regexp +} + +// NewExpectedActual constructs ExpectedActual checker using DefaultExpectedVarPattern. +func NewExpectedActual() *ExpectedActual { + return &ExpectedActual{expVarPattern: DefaultExpectedVarPattern} +} + +func (ExpectedActual) Name() string { return "expected-actual" } + +func (checker *ExpectedActual) SetExpVarPattern(p *regexp.Regexp) *ExpectedActual { + if p != nil { + checker.expVarPattern = p + } + return checker +} + +func (checker ExpectedActual) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + switch call.Fn.Name { + case "Equal", "Equalf", "NotEqual", "NotEqualf", + "JSONEq", "JSONEqf", "YAMLEq", "YAMLEqf": + default: + return nil + } + + if len(call.Args) < 2 { + return nil + } + first, second := call.Args[0], call.Args[1] + + if checker.isWrongExpectedActualOrder(pass, first, second) { + return newDiagnostic(checker.Name(), call, "need to reverse actual and expected values", &analysis.SuggestedFix{ + Message: "Reverse actual and expected values", + TextEdits: []analysis.TextEdit{ + { + Pos: first.Pos(), + End: second.End(), + NewText: formatAsCallArgs(pass, second, first), + }, + }, + }) + } + return nil +} + +func (checker ExpectedActual) isWrongExpectedActualOrder(pass *analysis.Pass, first, second ast.Expr) bool { + leftIsCandidate := checker.isExpectedValueCandidate(pass, first) + rightIsCandidate := checker.isExpectedValueCandidate(pass, second) + return rightIsCandidate && !leftIsCandidate +} + +func (checker ExpectedActual) isExpectedValueCandidate(pass *analysis.Pass, expr ast.Expr) bool { + switch v := expr.(type) { + case *ast.CompositeLit: + return true + + case *ast.CallExpr: + return isCastedBasicLitOrExpectedValue(v, checker.expVarPattern) || + isExpectedValueFactory(v, checker.expVarPattern) + } + + return isBasicLit(expr) || + isUntypedConst(pass, expr) || + isTypedConst(pass, expr) || + isIdentNamedAsExpected(checker.expVarPattern, expr) || + isStructFieldNamedAsExpected(checker.expVarPattern, expr) +} + +func isCastedBasicLitOrExpectedValue(ce *ast.CallExpr, pattern *regexp.Regexp) bool { + if len(ce.Args) != 1 { + return false + } + + fn, ok := ce.Fun.(*ast.Ident) + if !ok { + return false + } + + switch fn.Name { + case "complex64", "complex128": + return true + + case "uint", "uint8", "uint16", "uint32", "uint64", + "int", "int8", "int16", "int32", "int64", + "float32", "float64", + "rune", "string": + return isBasicLit(ce.Args[0]) || isIdentNamedAsExpected(pattern, ce.Args[0]) + } + return false +} + +func isExpectedValueFactory(ce *ast.CallExpr, pattern *regexp.Regexp) bool { + if len(ce.Args) != 0 { + return false + } + + switch fn := ce.Fun.(type) { + case *ast.Ident: + return pattern.MatchString(fn.Name) + case *ast.SelectorExpr: + return pattern.MatchString(fn.Sel.Name) + } + return false +} + +func isBasicLit(e ast.Expr) bool { + _, ok := e.(*ast.BasicLit) + return ok +} + +func isUntypedConst(p *analysis.Pass, e ast.Expr) bool { + t := p.TypesInfo.TypeOf(e) + if t == nil { + return false + } + + b, ok := t.(*types.Basic) + return ok && b.Info()&types.IsUntyped > 0 +} + +func isTypedConst(p *analysis.Pass, e ast.Expr) bool { + tt, ok := p.TypesInfo.Types[e] + return ok && tt.IsValue() && tt.Value != nil +} + +func isIdentNamedAsExpected(pattern *regexp.Regexp, e ast.Expr) bool { + id, ok := e.(*ast.Ident) + return ok && pattern.MatchString(id.Name) +} + +func isStructFieldNamedAsExpected(pattern *regexp.Regexp, e ast.Expr) bool { + s, ok := e.(*ast.SelectorExpr) + return ok && isIdentNamedAsExpected(pattern, s.Sel) +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/float_compare.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/float_compare.go new file mode 100644 index 000000000..7d5b358b3 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/float_compare.go @@ -0,0 +1,63 @@ +package checkers + +import ( + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" +) + +// FloatCompare detects situation like +// +// assert.Equal(t, 42.42, a) +// assert.True(t, a == 42.42) +// assert.False(t, a != 42.42) +// +// and requires +// +// assert.InEpsilon(t, 42.42, a, 0.0001) // Or assert.InDelta +type FloatCompare struct{} + +// NewFloatCompare constructs FloatCompare checker. +func NewFloatCompare() FloatCompare { return FloatCompare{} } +func (FloatCompare) Name() string { return "float-compare" } + +func (checker FloatCompare) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + invalid := func() bool { + switch call.Fn.Name { + case "Equal", "Equalf": + return len(call.Args) > 1 && isFloat(pass, call.Args[0]) && isFloat(pass, call.Args[1]) + + case "True", "Truef": + return len(call.Args) > 0 && isFloatCompare(pass, call.Args[0], token.EQL) + + case "False", "Falsef": + return len(call.Args) > 0 && isFloatCompare(pass, call.Args[0], token.NEQ) + } + return false + }() + + if invalid { + return newUseFunctionDiagnostic(checker.Name(), call, "InEpsilon (or InDelta)", nil) + } + return nil +} + +func isFloat(pass *analysis.Pass, expr ast.Expr) bool { + t := pass.TypesInfo.TypeOf(expr) + if t == nil { + return false + } + + bt, ok := t.Underlying().(*types.Basic) + return ok && (bt.Info()&types.IsFloat > 0) +} + +func isFloatCompare(p *analysis.Pass, e ast.Expr, op token.Token) bool { + be, ok := e.(*ast.BinaryExpr) + if !ok { + return false + } + return be.Op == op && (isFloat(p, be.X) || isFloat(p, be.Y)) +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/len.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/len.go new file mode 100644 index 000000000..f10412f63 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/len.go @@ -0,0 +1,86 @@ +package checkers + +import ( + "go/ast" + "go/token" + + "golang.org/x/tools/go/analysis" +) + +// Len detects situations like +// +// assert.Equal(t, 3, len(arr)) +// assert.True(t, len(arr) == 3) +// +// and requires +// +// assert.Len(t, arr, 3) +type Len struct{} + +// NewLen constructs Len checker. +func NewLen() Len { return Len{} } +func (Len) Name() string { return "len" } + +func (checker Len) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + const proposedFn = "Len" + + switch call.Fn.Name { + case "Equal", "Equalf": + if len(call.Args) < 2 { + return nil + } + a, b := call.Args[0], call.Args[1] + + if lenArg, expectedLen, ok := xorLenCall(pass, a, b); ok { + return newUseFunctionDiagnostic(checker.Name(), call, proposedFn, + newSuggestedFuncReplacement(call, proposedFn, analysis.TextEdit{ + Pos: a.Pos(), + End: b.End(), + NewText: formatAsCallArgs(pass, lenArg, expectedLen), + }), + ) + } + + case "True", "Truef": + if len(call.Args) < 1 { + return nil + } + expr := call.Args[0] + + if lenArg, expectedLen, ok := isLenEquality(pass, expr); ok { + return newUseFunctionDiagnostic(checker.Name(), call, proposedFn, + newSuggestedFuncReplacement(call, proposedFn, analysis.TextEdit{ + Pos: expr.Pos(), + End: expr.End(), + NewText: formatAsCallArgs(pass, lenArg, expectedLen), + }), + ) + } + } + return nil +} + +func xorLenCall(pass *analysis.Pass, a, b ast.Expr) (lenArg ast.Expr, expectedLen ast.Expr, ok bool) { + arg1, ok1 := isBuiltinLenCall(pass, a) + arg2, ok2 := isBuiltinLenCall(pass, b) + + if xor(ok1, ok2) { + if ok1 { + return arg1, b, true + } + return arg2, a, true + } + return nil, nil, false +} + +func isLenEquality(pass *analysis.Pass, e ast.Expr) (ast.Expr, ast.Expr, bool) { + be, ok := e.(*ast.BinaryExpr) + if !ok { + return nil, nil, false + } + + if be.Op != token.EQL { + return nil, nil, false + } + return xorLenCall(pass, be.X, be.Y) +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/require_error.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/require_error.go new file mode 100644 index 000000000..2da71ed81 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/require_error.go @@ -0,0 +1,35 @@ +package checkers + +import "golang.org/x/tools/go/analysis" + +// RequireError detects situations like +// +// assert.NoError(t, err) +// s.ErrorIs(err, io.EOF) +// s.Assert().Error(err) +// +// and requires +// +// require.NoError(t, err) +// s.Require().ErrorIs(err, io.EOF) +// s.Require().Error(err) +type RequireError struct{} + +// NewRequireError constructs RequireError checker. +func NewRequireError() RequireError { return RequireError{} } +func (RequireError) Name() string { return "require-error" } + +func (checker RequireError) Check(_ *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + if !call.IsAssert { + return nil + } + + const msg = "for error assertions use require" + + switch call.Fn.Name { + case "Error", "ErrorIs", "ErrorAs", "EqualError", "ErrorContains", "NoError", "NotErrorIs", + "Errorf", "ErrorIsf", "ErrorAsf", "EqualErrorf", "ErrorContainsf", "NoErrorf", "NotErrorIsf": + return newDiagnostic(checker.Name(), call, msg, nil) + } + return nil +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/suite_dont_use_pkg.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/suite_dont_use_pkg.go new file mode 100644 index 000000000..bf84f6378 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/suite_dont_use_pkg.go @@ -0,0 +1,96 @@ +package checkers + +import ( + "fmt" + "go/ast" + "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/Antonboom/testifylint/internal/analysisutil" + "github.com/Antonboom/testifylint/internal/testify" +) + +// SuiteDontUsePkg detects situation like +// +// func (s *MySuite) TestSomething() { +// assert.Equal(s.T(), 42, value) +// } +// +// and requires +// +// func (s *MySuite) TestSomething() { +// s.Equal(42, value) +// } +type SuiteDontUsePkg struct{} + +// NewSuiteDontUsePkg constructs SuiteDontUsePkg checker. +func NewSuiteDontUsePkg() SuiteDontUsePkg { return SuiteDontUsePkg{} } +func (SuiteDontUsePkg) Name() string { return "suite-dont-use-pkg" } + +func (checker SuiteDontUsePkg) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + if !call.IsPkg { + return nil + } + + args := call.ArgsRaw + if len(args) < 2 { + return nil + } + t := args[0] + + ce, ok := t.(*ast.CallExpr) + if !ok { + return nil + } + se, ok := ce.Fun.(*ast.SelectorExpr) + if !ok { + return nil + } + if se.X == nil || !implementsTestifySuiteIface(pass, se.X) { + return nil + } + if se.Sel == nil || se.Sel.Name != "T" { + return nil + } + rcv, ok := se.X.(*ast.Ident) // At this point we ensure that `s.T()` is used as the first argument of assertion. + if !ok { + return nil + } + + newSelector := rcv.Name + if !call.IsAssert { + newSelector += "." + "Require()" + } + + msg := fmt.Sprintf("use %s.%s", newSelector, call.Fn.Name) + return newDiagnostic(checker.Name(), call, msg, &analysis.SuggestedFix{ + Message: fmt.Sprintf("Replace `%s` with `%s`", call.SelectorXStr, newSelector), + TextEdits: []analysis.TextEdit{ + // Replace package function with suite method. + { + Pos: call.Selector.X.Pos(), + End: call.Selector.X.End(), + NewText: []byte(newSelector), + }, + // Remove `s.T()`. + { + Pos: t.Pos(), + End: args[1].Pos(), + NewText: []byte(""), + }, + }, + }) +} + +func implementsTestifySuiteIface(pass *analysis.Pass, rcv ast.Expr) bool { + suiteIface := analysisutil.ObjectOf(pass.Pkg, testify.SuitePkgPath, "TestingSuite") + if suiteIface == nil { + return false + } + + return types.Implements( + pass.TypesInfo.TypeOf(rcv), + suiteIface.Type().Underlying().(*types.Interface), + ) +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/suite_extra_assert_call.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/suite_extra_assert_call.go new file mode 100644 index 000000000..791488b65 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/suite_extra_assert_call.go @@ -0,0 +1,99 @@ +package checkers + +import ( + "fmt" + "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/Antonboom/testifylint/internal/analysisutil" +) + +// SuiteExtraAssertCallMode reflects different modes of work of SuiteExtraAssertCall checker. +type SuiteExtraAssertCallMode int + +const ( + SuiteExtraAssertCallModeRemove SuiteExtraAssertCallMode = iota + SuiteExtraAssertCallModeRequire +) + +const DefaultSuiteExtraAssertCallMode = SuiteExtraAssertCallModeRemove + +// SuiteExtraAssertCall detects situation like +// +// func (s *MySuite) TestSomething() { +// s.Assert().Equal(42, value) +// } +// +// and requires +// +// func (s *MySuite) TestSomething() { +// s.Equal(42, value) +// } +// +// or vice versa (depending on the configurable mode). +type SuiteExtraAssertCall struct { + mode SuiteExtraAssertCallMode +} + +// NewSuiteExtraAssertCall constructs SuiteExtraAssertCall checker. +func NewSuiteExtraAssertCall() *SuiteExtraAssertCall { + return &SuiteExtraAssertCall{mode: DefaultSuiteExtraAssertCallMode} +} + +func (SuiteExtraAssertCall) Name() string { return "suite-extra-assert-call" } + +func (checker *SuiteExtraAssertCall) SetMode(m SuiteExtraAssertCallMode) *SuiteExtraAssertCall { + checker.mode = m + return checker +} + +func (checker SuiteExtraAssertCall) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { + if call.IsPkg { + return nil + } + + switch checker.mode { + case SuiteExtraAssertCallModeRequire: + x, ok := call.Selector.X.(*ast.Ident) // s.True + if !ok || x == nil || !implementsTestifySuiteIface(pass, x) { + return nil + } + + msg := fmt.Sprintf("use an explicit %s.Assert().%s", analysisutil.NodeString(pass.Fset, x), call.Fn.Name) + return newDiagnostic(checker.Name(), call, msg, &analysis.SuggestedFix{ + Message: "Add `Assert()` call", + TextEdits: []analysis.TextEdit{{ + Pos: x.End(), + End: x.End(), // Pure insertion. + NewText: []byte(".Assert()"), + }}, + }) + + case SuiteExtraAssertCallModeRemove: + x, ok := call.Selector.X.(*ast.CallExpr) // s.Assert().True + if !ok { + return nil + } + + se, ok := x.Fun.(*ast.SelectorExpr) + if !ok || se == nil || !implementsTestifySuiteIface(pass, se.X) { + return nil + } + if se.Sel == nil || se.Sel.Name != "Assert" { + return nil + } + + msg := fmt.Sprintf("need to simplify the assertion to %s.%s", analysisutil.NodeString(pass.Fset, se.X), call.Fn.Name) + return newDiagnostic(checker.Name(), call, msg, &analysis.SuggestedFix{ + Message: "Remove `Assert()` call", + TextEdits: []analysis.TextEdit{{ + Pos: se.Sel.Pos(), + End: x.End() + 1, // +1 for dot. + NewText: []byte(""), + }}, + }) + } + + return nil +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/checkers/suite_thelper.go b/vendor/github.com/Antonboom/testifylint/internal/checkers/suite_thelper.go new file mode 100644 index 000000000..5cadc93ad --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/checkers/suite_thelper.go @@ -0,0 +1,130 @@ +package checkers + +import ( + "fmt" + "go/ast" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" + + "github.com/Antonboom/testifylint/internal/analysisutil" + "github.com/Antonboom/testifylint/internal/testify" +) + +// SuiteTHelper requires t.Helper() call in suite helpers: +// +// func (s *RoomSuite) assertRoomRound(roundID RoundID) { +// s.T().Helper() +// s.Equal(roundID, s.getRoom().CurrentRound.ID) +// } +type SuiteTHelper struct{} + +// NewSuiteTHelper constructs SuiteTHelper checker. +func NewSuiteTHelper() SuiteTHelper { return SuiteTHelper{} } +func (SuiteTHelper) Name() string { return "suite-thelper" } + +func (checker SuiteTHelper) Check(pass *analysis.Pass, inspector *inspector.Inspector) (diagnostics []analysis.Diagnostic) { + inspector.Preorder([]ast.Node{(*ast.FuncDecl)(nil)}, func(node ast.Node) { + fd := node.(*ast.FuncDecl) + if !isTestifySuiteMethod(pass, fd) { + return + } + + if ident := fd.Name; ident == nil || isTestMethod(ident.Name) || isServiceMethod(ident.Name) { + return + } + + if !containsSuiteAssertions(pass, fd) { + return + } + + rcv := fd.Recv.List[0] + if len(rcv.Names) != 1 || rcv.Names[0] == nil { + return + } + rcvName := rcv.Names[0].Name + + helperCallStr := fmt.Sprintf("%s.T().Helper()", rcvName) + + firstStmt := fd.Body.List[0] + if analysisutil.NodeString(pass.Fset, firstStmt) == helperCallStr { + return + } + + msg := fmt.Sprintf("suite helper method must start with " + helperCallStr) + d := newDiagnostic(checker.Name(), fd, msg, &analysis.SuggestedFix{ + Message: fmt.Sprintf("Insert `%s`", helperCallStr), + TextEdits: []analysis.TextEdit{ + { + Pos: firstStmt.Pos(), + End: firstStmt.Pos(), // Pure insertion. + NewText: []byte(helperCallStr + "\n\n"), + }, + }, + }) + diagnostics = append(diagnostics, *d) + }) + return diagnostics +} + +func isTestifySuiteMethod(pass *analysis.Pass, fDecl *ast.FuncDecl) bool { + if fDecl.Recv == nil || len(fDecl.Recv.List) != 1 { + return false + } + + rcv := fDecl.Recv.List[0] + return implementsTestifySuiteIface(pass, rcv.Type) +} + +func isTestMethod(name string) bool { + return strings.HasPrefix(name, "Test") +} + +func isServiceMethod(name string) bool { + // https://github.com/stretchr/testify/blob/master/suite/interfaces.go + switch name { + case "T", "SetT", "SetS", "SetupSuite", "SetupTest", "TearDownSuite", "TearDownTest", + "BeforeTest", "AfterTest", "HandleStats", "SetupSubTest", "TearDownSubTest": + return true + } + return false +} + +func containsSuiteAssertions(pass *analysis.Pass, fn *ast.FuncDecl) bool { + if fn.Body == nil { + return false + } + + for _, s := range fn.Body.List { + if isSuiteAssertion(pass, s) { + return true + } + } + return false +} + +func isSuiteAssertion(pass *analysis.Pass, stmt ast.Stmt) bool { + expr, ok := stmt.(*ast.ExprStmt) + if !ok { + return false + } + + ce, ok := expr.X.(*ast.CallExpr) + if !ok { + return false + } + + se, ok := ce.Fun.(*ast.SelectorExpr) + if !ok || se.Sel == nil { + return false + } + + if sel, ok := pass.TypesInfo.Selections[se]; ok { + pkg := sel.Obj().Pkg() + isAssert := analysisutil.IsPkg(pkg, testify.AssertPkgName, testify.AssertPkgPath) + isRequire := analysisutil.IsPkg(pkg, testify.RequirePkgName, testify.RequirePkgPath) + return isAssert || isRequire + } + return false +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/config/config.go b/vendor/github.com/Antonboom/testifylint/internal/config/config.go new file mode 100644 index 000000000..51f627008 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/config/config.go @@ -0,0 +1,53 @@ +package config + +import ( + "flag" + + "github.com/Antonboom/testifylint/internal/checkers" +) + +// NewDefault builds default testifylint config. +func NewDefault() Config { + return Config{ + EnableAll: false, + EnabledCheckers: checkers.EnabledByDefault(), + ExpectedActual: ExpectedActualConfig{ + ExpVarPattern: RegexpValue{checkers.DefaultExpectedVarPattern}, + }, + SuiteExtraAssertCall: SuiteExtraAssertCallConfig{ + Mode: checkers.DefaultSuiteExtraAssertCallMode, + }, + } +} + +// Config implements testifylint configuration. +type Config struct { + EnableAll bool + EnabledCheckers KnownCheckersValue + ExpectedActual ExpectedActualConfig + SuiteExtraAssertCall SuiteExtraAssertCallConfig +} + +// ExpectedActualConfig implements configuration of checkers.ExpectedActual. +type ExpectedActualConfig struct { + ExpVarPattern RegexpValue +} + +// SuiteExtraAssertCallConfig implements configuration of checkers.SuiteExtraAssertCall. +type SuiteExtraAssertCallConfig struct { + Mode checkers.SuiteExtraAssertCallMode +} + +// BindToFlags binds Config fields to according flags. +func BindToFlags(cfg *Config, fs *flag.FlagSet) { + fs.BoolVar(&cfg.EnableAll, "enable-all", false, "enable all checkers") + fs.Var(&cfg.EnabledCheckers, "enable", "comma separated list of enabled checkers") + fs.Var(&cfg.ExpectedActual.ExpVarPattern, "expected-actual.pattern", "regexp for expected variable name") + fs.Var(NewEnumValue(suiteExtraAssertCallModeAsString, &cfg.SuiteExtraAssertCall.Mode), + "suite-extra-assert-call.mode", "to require or remove extra Assert() call") +} + +var suiteExtraAssertCallModeAsString = map[string]checkers.SuiteExtraAssertCallMode{ + "remove": checkers.SuiteExtraAssertCallModeRemove, + "require": checkers.SuiteExtraAssertCallModeRequire, +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/config/flag_value_types.go b/vendor/github.com/Antonboom/testifylint/internal/config/flag_value_types.go new file mode 100644 index 000000000..2f0ee978f --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/config/flag_value_types.go @@ -0,0 +1,105 @@ +package config + +import ( + "flag" + "fmt" + "regexp" + "sort" + "strings" + + "github.com/Antonboom/testifylint/internal/checkers" +) + +var ( + _ flag.Value = (*KnownCheckersValue)(nil) + _ flag.Value = (*RegexpValue)(nil) + _ flag.Value = (*EnumValue[checkers.SuiteExtraAssertCallMode])(nil) +) + +// KnownCheckersValue implements comma separated list of testify checkers. +type KnownCheckersValue []string + +func (kcv KnownCheckersValue) String() string { + return strings.Join(kcv, ",") +} + +func (kcv *KnownCheckersValue) Set(v string) error { + chckrs := strings.Split(v, ",") + for _, checkerName := range chckrs { + if ok := checkers.IsKnown(checkerName); !ok { + return fmt.Errorf("unknown checker %q", checkerName) + } + } + + *kcv = chckrs + return nil +} + +// RegexpValue is a special wrapper for support of flag.FlagSet over regexp.Regexp. +// Original regexp is available through RegexpValue.Regexp. +type RegexpValue struct { + *regexp.Regexp +} + +func (rv RegexpValue) String() string { + if rv.Regexp == nil { + return "" + } + return rv.Regexp.String() +} + +func (rv *RegexpValue) Set(v string) error { + compiled, err := regexp.Compile(v) + if err != nil { + return err + } + + rv.Regexp = compiled + return nil +} + +// EnumValue is a special type for support of flag.FlagSet over user-defined constants. +type EnumValue[EnumT comparable] struct { + mapping map[string]EnumT + keys []string + dst *EnumT +} + +// NewEnumValue takes the "enum-value-name to enum-value" mapping and a destination for the value passed through the CLI. +// Returns an EnumValue instance suitable for flag.FlagSet.Var. +func NewEnumValue[EnumT comparable](mapping map[string]EnumT, dst *EnumT) *EnumValue[EnumT] { + keys := make([]string, 0, len(mapping)) + for k := range mapping { + keys = append(keys, k) + } + sort.Strings(keys) + + return &EnumValue[EnumT]{ + mapping: mapping, + keys: keys, + dst: dst, + } +} + +func (e EnumValue[EnumT]) String() string { + if e.dst == nil { + return "" + } + + for k, v := range e.mapping { + if v == *e.dst { + return k + } + } + return "" +} + +func (e *EnumValue[EnumT]) Set(s string) error { + v, ok := e.mapping[s] + if !ok { + return fmt.Errorf("use one of (%v)", strings.Join(e.keys, " | ")) + } + + *e.dst = v + return nil +} diff --git a/vendor/github.com/Antonboom/testifylint/internal/testify/const.go b/vendor/github.com/Antonboom/testifylint/internal/testify/const.go new file mode 100644 index 000000000..45731aa97 --- /dev/null +++ b/vendor/github.com/Antonboom/testifylint/internal/testify/const.go @@ -0,0 +1,13 @@ +package testify + +const ( + ModulePath = "github.com/stretchr/testify" + + AssertPkgName = "assert" + RequirePkgName = "require" + SuitePkgName = "suite" + + AssertPkgPath = ModulePath + "/" + AssertPkgName + RequirePkgPath = ModulePath + "/" + RequirePkgName + SuitePkgPath = ModulePath + "/" + SuitePkgName +) -- cgit mrf-deployment