From 27e76fae2ee2d84dc7db63af1d9ed7358ba35b7a Mon Sep 17 00:00:00 2001 From: Taras Madan Date: Mon, 11 Nov 2024 11:41:38 +0100 Subject: vendor: update --- vendor/github.com/nunnatsa/ginkgolinter/.gitignore | 1 + vendor/github.com/nunnatsa/ginkgolinter/Makefile | 12 +- vendor/github.com/nunnatsa/ginkgolinter/README.md | 46 + .../github.com/nunnatsa/ginkgolinter/analyzer.go | 16 +- vendor/github.com/nunnatsa/ginkgolinter/doc.go | 17 + .../internal/expression/actual/actual.go | 118 ++ .../internal/expression/actual/actualarg.go | 235 +++ .../internal/expression/actual/asyncactual.go | 123 ++ .../internal/expression/actual/asyncfuncarg.go | 38 + .../expression/actual/comparisonAsserion.go | 260 ++++ .../ginkgolinter/internal/expression/expression.go | 315 ++++ .../internal/expression/matcher/bematchers.go | 77 + .../expression/matcher/benumericmatcher.go | 128 ++ .../internal/expression/matcher/equalmatcher.go | 124 ++ .../internal/expression/matcher/errormatchers.go | 199 +++ .../internal/expression/matcher/lenmatchers.go | 11 + .../internal/expression/matcher/matcher.go | 86 ++ .../internal/expression/matcher/matcherinfo.go | 148 ++ .../internal/expression/matcher/matcherwithnest.go | 66 + .../expression/matcher/multiplematchers.go | 62 + .../internal/expression/value/value.go | 221 +++ .../ginkgolinter/internal/formatter/formatter.go | 22 + .../internal/ginkgohandler/dothandler.go | 36 + .../internal/ginkgohandler/ginkgoinfo.go | 63 + .../ginkgolinter/internal/ginkgohandler/handler.go | 123 +- .../internal/ginkgohandler/handling.go | 195 +++ .../internal/ginkgohandler/namehandler.go | 49 + .../internal/gomegahandler/dothandler.go | 99 ++ .../ginkgolinter/internal/gomegahandler/handler.go | 215 +-- .../internal/gomegahandler/namedhandler.go | 112 ++ .../ginkgolinter/internal/gomegainfo/gomegainfo.go | 113 ++ .../ginkgolinter/internal/interfaces/interfaces.go | 2 +- .../ginkgolinter/internal/intervals/intervals.go | 339 ++--- .../internal/reports/report-builder.go | 28 +- .../internal/rules/asyncfunccallrule.go | 41 + .../internal/rules/asyncsucceedrule.go | 30 + .../internal/rules/asynctimeintervalsrule.go | 79 + .../ginkgolinter/internal/rules/caprule.go | 128 ++ .../internal/rules/comparepointerrule.go | 64 + .../ginkgolinter/internal/rules/comparisonrule.go | 75 + .../internal/rules/doublenegativerule.go | 30 + .../ginkgolinter/internal/rules/equalboolrule.go | 36 + .../internal/rules/equaldifferenttypesrule.go | 119 ++ .../ginkgolinter/internal/rules/equalnilrule.go | 29 + .../internal/rules/errorequalnilrule.go | 35 + .../internal/rules/forceexpecttorule.go | 43 + .../ginkgolinter/internal/rules/havelen0.go | 23 + .../internal/rules/haveoccurredrule.go | 35 + .../ginkgolinter/internal/rules/lenrule.go | 119 ++ .../ginkgolinter/internal/rules/matcheronlyrule.go | 12 + .../ginkgolinter/internal/rules/matcherrorrule.go | 110 ++ .../internal/rules/missingassertionrule.go | 27 + .../ginkgolinter/internal/rules/nilcomparerule.go | 75 + .../nunnatsa/ginkgolinter/internal/rules/rule.go | 61 + .../ginkgolinter/internal/rules/succeedrule.go | 41 + .../nunnatsa/ginkgolinter/linter/ginkgo_linter.go | 1604 +------------------- .../nunnatsa/ginkgolinter/types/config.go | 2 + 57 files changed, 4387 insertions(+), 2130 deletions(-) create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/actual.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/actualarg.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/asyncactual.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/asyncfuncarg.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/comparisonAsserion.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/expression.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/bematchers.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/benumericmatcher.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/equalmatcher.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/errormatchers.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/lenmatchers.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcher.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcherinfo.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcherwithnest.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/multiplematchers.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/expression/value/value.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/formatter/formatter.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/dothandler.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/ginkgoinfo.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/handling.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/namehandler.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/dothandler.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/namedhandler.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/gomegainfo/gomegainfo.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asyncfunccallrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asyncsucceedrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asynctimeintervalsrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/caprule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/comparepointerrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/comparisonrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/doublenegativerule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equalboolrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equaldifferenttypesrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equalnilrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/errorequalnilrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/forceexpecttorule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/havelen0.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/haveoccurredrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/lenrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/matcheronlyrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/matcherrorrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/missingassertionrule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/nilcomparerule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/rule.go create mode 100644 vendor/github.com/nunnatsa/ginkgolinter/internal/rules/succeedrule.go (limited to 'vendor/github.com/nunnatsa') diff --git a/vendor/github.com/nunnatsa/ginkgolinter/.gitignore b/vendor/github.com/nunnatsa/ginkgolinter/.gitignore index 7d7f8b10c..67467b717 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/.gitignore +++ b/vendor/github.com/nunnatsa/ginkgolinter/.gitignore @@ -1,2 +1,3 @@ ginkgolinter bin/ +e2e \ No newline at end of file diff --git a/vendor/github.com/nunnatsa/ginkgolinter/Makefile b/vendor/github.com/nunnatsa/ginkgolinter/Makefile index 586633006..8ddd8c42c 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/Makefile +++ b/vendor/github.com/nunnatsa/ginkgolinter/Makefile @@ -5,7 +5,7 @@ HASH_FLAG := -X github.com/nunnatsa/ginkgolinter/version.gitHash=$(COMMIT_HASH) BUILD_ARGS := -ldflags "$(VERSION_FLAG) $(HASH_FLAG)" -build: unit-test +build: goimports go build $(BUILD_ARGS) -o ginkgolinter ./cmd/ginkgolinter unit-test: @@ -23,5 +23,11 @@ build-for-linux: build-all: build build-for-linux build-for-mac build-for-windows -test: build - ./tests/e2e.sh +test-cli: + cd tests; go test -v ./ + +test: unit-test test-cli + +goimports: + go install golang.org/x/tools/cmd/goimports@latest + goimports -w -local="github.com/nunnatsa/ginkgolinter" $(shell find . -type f -name '*.go' ! -path "*/vendor/*") diff --git a/vendor/github.com/nunnatsa/ginkgolinter/README.md b/vendor/github.com/nunnatsa/ginkgolinter/README.md index 977cec903..536a65e7b 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/README.md +++ b/vendor/github.com/nunnatsa/ginkgolinter/README.md @@ -249,6 +249,28 @@ This will probably happen when using the old format: Eventually(aFunc, 500 * time.Millisecond /*timeout*/, 10 * time.Second /*polling*/).Should(Succeed()) ``` +### Correct usage of the `Succeed()` matcher [Bug] +The `Succeed()` matcher only accepts a single error value. this rule validates that. + +For example: + ```go + Expect(42).To(Succeed()) + ``` + +But mostly, we want to avoid using this matcher with functions that return multiple values, even if their last +returned value is an error, because this is not supported: + ```go + Expect(os.Open("myFile.txt")).To(Succeed()) + ``` + +In async assertions (like `Eventually()`), the `Succeed()` matcher may also been used with functions that accept +a Gomega object as their first parameter, and returns nothing, e.g. this is a valid usage of `Eventually` + ```go + Eventually(func(g Gomega){ + g.Expect(true).To(BeTrue()) + }).WithTimeout(10 * time.Millisecond).WithPolling(time.Millisecond).Should(Succeed()) + ``` + ### Avoid Spec Pollution: Don't Initialize Variables in Container Nodes [BUG/STYLE]: ***Note***: Only applied when the `--forbid-spec-pollution=true` flag is set (disabled by default). @@ -476,6 +498,30 @@ will be changed to: ```go Eventually(aFunc, time.Second*5, time.Second*polling) ``` + +### Correct usage of the `Succeed()` and the `HaveOccurred()` matchers +This rule enforces using the `Success()` matcher only for functions, and the `HaveOccurred()` matcher only for error +values. + +For example: + ```go + Expect(err).To(Succeed()) + ``` +will trigger a warning with a suggestion to replace the mather to + ```go + Expect(err).ToNot(HaveOccurred()) + ``` + +and vice versa: + ```go + Expect(myErrorFunc()).ToNot(HaveOccurred()) + ``` +will trigger a warning with a suggestion to replace the mather to + ```go + Expect(myErrorFunc()).To(Succeed()) + ``` +***This rule is disabled by default***. Use the `--force-succeed=true` command line flag to enable it. + ## Suppress the linter ### Suppress warning from command line * Use the `--suppress-len-assertion=true` flag to suppress the wrong length and cap assertions warning diff --git a/vendor/github.com/nunnatsa/ginkgolinter/analyzer.go b/vendor/github.com/nunnatsa/ginkgolinter/analyzer.go index edff57acd..dbc39aba5 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/analyzer.go +++ b/vendor/github.com/nunnatsa/ginkgolinter/analyzer.go @@ -25,13 +25,14 @@ func NewAnalyzerWithConfig(config *types.Config) *analysis.Analyzer { // NewAnalyzer returns an Analyzer - the package interface with nogo func NewAnalyzer() *analysis.Analyzer { config := &types.Config{ - SuppressLen: false, - SuppressNil: false, - SuppressErr: false, - SuppressCompare: false, - ForbidFocus: false, - AllowHaveLen0: false, - ForceExpectTo: false, + SuppressLen: false, + SuppressNil: false, + SuppressErr: false, + SuppressCompare: false, + ForbidFocus: false, + AllowHaveLen0: false, + ForceExpectTo: false, + ForceSucceedForFuncs: false, } a := NewAnalyzerWithConfig(config) @@ -50,6 +51,7 @@ func NewAnalyzer() *analysis.Analyzer { 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") a.Flags.Var(&config.ForbidFocus, "forbid-focus-container", "trigger a warning for ginkgo focus containers like FDescribe, FContext, FWhen or FIt; default = false.") 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.") + a.Flags.Var(&config.ForceSucceedForFuncs, "force-succeed", "force using the Succeed matcher for error functions, and the HaveOccurred matcher for non-function error values") return a } diff --git a/vendor/github.com/nunnatsa/ginkgolinter/doc.go b/vendor/github.com/nunnatsa/ginkgolinter/doc.go index dd9ecf58a..c07b6a316 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/doc.go +++ b/vendor/github.com/nunnatsa/ginkgolinter/doc.go @@ -30,6 +30,14 @@ For example: This will probably happen when using the old format: Eventually(aFunc, 500 * time.Millisecond, 10 * time.Second).Should(Succeed()) +* Success matcher validation: [BUG] + The Success matcher expect that the actual argument will be a single error. In async actual assertions, It also allow + functions with Gomega object as the function first parameter. +For example: + Expect(myInt).To(Succeed()) +or + Eventually(func() int { return 42 }).Should(Succeed()) + * reject variable assignments in ginkgo containers [Bug/Style]: For example: var _ = Describe("description", func(){ @@ -96,4 +104,13 @@ methods. For example: Eventually(context.Background(), func() bool { return true }, "1s").Should(BeTrue()) Eventually(context.Background(), func() bool { return true }, time.Second*60, 15).Should(BeTrue()) + +* Success <=> Eventually usage [Style] + enforce that the Succeed() matcher will be used for error functions, and the HaveOccurred() matcher will + be used for error values. + +For example: + Expect(err).ToNot(Succeed()) +or + Expect(funcRetError().ToNot(HaveOccurred()) ` diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/actual.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/actual.go new file mode 100644 index 000000000..8e3df5d3f --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/actual.go @@ -0,0 +1,118 @@ +package actual + +import ( + "go/ast" + gotypes "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/gomegahandler" + "github.com/nunnatsa/ginkgolinter/internal/gomegainfo" +) + +type Actual struct { + Orig *ast.CallExpr + Clone *ast.CallExpr + Arg ArgPayload + argType gotypes.Type + isTuple bool + isAsync bool + asyncArg *AsyncArg + actualOffset int +} + +func New(origExpr, cloneExpr *ast.CallExpr, orig *ast.CallExpr, clone *ast.CallExpr, pass *analysis.Pass, handler gomegahandler.Handler, timePkg string) (*Actual, bool) { + funcName, ok := handler.GetActualFuncName(orig) + if !ok { + return nil, false + } + + arg, actualOffset := getActualArgPayload(orig, clone, pass, funcName) + if arg == nil { + return nil, false + } + + argType := pass.TypesInfo.TypeOf(orig.Args[actualOffset]) + isTuple := false + + if tpl, ok := argType.(*gotypes.Tuple); ok { + if tpl.Len() > 0 { + argType = tpl.At(0).Type() + } else { + argType = nil + } + + isTuple = tpl.Len() > 1 + } + + isAsyncExpr := gomegainfo.IsAsyncActualMethod(funcName) + + var asyncArg *AsyncArg + if isAsyncExpr { + asyncArg = newAsyncArg(origExpr, cloneExpr, orig, clone, argType, pass, actualOffset, timePkg) + } + + return &Actual{ + Orig: orig, + Clone: clone, + Arg: arg, + argType: argType, + isTuple: isTuple, + isAsync: isAsyncExpr, + asyncArg: asyncArg, + actualOffset: actualOffset, + }, true +} + +func (a *Actual) ReplaceActual(newArgs ast.Expr) { + a.Clone.Args[a.actualOffset] = newArgs +} + +func (a *Actual) ReplaceActualWithItsFirstArg() { + firstArgOfArg := a.Clone.Args[a.actualOffset].(*ast.CallExpr).Args[0] + a.ReplaceActual(firstArgOfArg) +} + +func (a *Actual) IsAsync() bool { + return a.isAsync +} + +func (a *Actual) IsTuple() bool { + return a.isTuple +} + +func (a *Actual) ArgGOType() gotypes.Type { + return a.argType +} + +func (a *Actual) GetAsyncArg() *AsyncArg { + return a.asyncArg +} + +func (a *Actual) AppendWithArgsMethod() { + if a.asyncArg.fun != nil { + if len(a.asyncArg.fun.Args) > 0 { + actualOrigFunc := a.Clone.Fun + actualOrigArgs := a.Clone.Args + + actualOrigArgs[a.actualOffset] = a.asyncArg.fun.Fun + call := &ast.SelectorExpr{ + Sel: ast.NewIdent("WithArguments"), + X: &ast.CallExpr{ + Fun: actualOrigFunc, + Args: actualOrigArgs, + }, + } + + a.Clone.Fun = call + a.Clone.Args = a.asyncArg.fun.Args + a.Clone = a.Clone.Fun.(*ast.SelectorExpr).X.(*ast.CallExpr) + } else { + a.Clone.Args[a.actualOffset] = a.asyncArg.fun.Fun + } + } +} + +func (a *Actual) GetActualArg() ast.Expr { + return a.Clone.Args[a.actualOffset] +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/actualarg.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/actualarg.go new file mode 100644 index 000000000..9d251c468 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/actualarg.go @@ -0,0 +1,235 @@ +package actual + +import ( + "go/ast" + "go/token" + gotypes "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/expression/value" + "github.com/nunnatsa/ginkgolinter/internal/gomegainfo" + "github.com/nunnatsa/ginkgolinter/internal/reverseassertion" +) + +type ArgType uint64 + +const ( + UnknownActualArgType ArgType = 1 << iota + ErrActualArgType + LenFuncActualArgType + CapFuncActualArgType + ComparisonActualArgType + LenComparisonActualArgType + CapComparisonActualArgType + NilComparisonActualArgType + BinaryComparisonActualArgType + FuncSigArgType + ErrFuncActualArgType + GomegaParamArgType + MultiRetsArgType + + ErrorTypeArgType + + EqualZero + GreaterThanZero +) + +func (a ArgType) Is(val ArgType) bool { + return a&val != 0 +} + +func getActualArgPayload(origActualExpr, actualExprClone *ast.CallExpr, pass *analysis.Pass, actualMethodName string) (ArgPayload, int) { + origArgExpr, argExprClone, actualOffset, isGomegaExpr := getActualArg(origActualExpr, actualExprClone, actualMethodName, pass) + if !isGomegaExpr { + return nil, 0 + } + + var arg ArgPayload + + if value.IsExprError(pass, origArgExpr) { + arg = newErrPayload(origArgExpr, argExprClone, pass) + } else { + switch expr := origArgExpr.(type) { + case *ast.CallExpr: + arg = newFuncCallArgPayload(expr, argExprClone.(*ast.CallExpr)) + + case *ast.BinaryExpr: + arg = parseBinaryExpr(expr, argExprClone.(*ast.BinaryExpr), pass) + + default: + t := pass.TypesInfo.TypeOf(origArgExpr) + if sig, ok := t.(*gotypes.Signature); ok { + arg = getAsyncFuncArg(sig) + } + } + + } + + if arg != nil { + return arg, actualOffset + } + + return newRegularArgPayload(origArgExpr, argExprClone, pass), actualOffset +} + +func getActualArg(origActualExpr *ast.CallExpr, actualExprClone *ast.CallExpr, actualMethodName string, pass *analysis.Pass) (ast.Expr, ast.Expr, int, bool) { + var ( + origArgExpr ast.Expr + argExprClone ast.Expr + ) + + funcOffset := gomegainfo.ActualArgOffset(actualMethodName) + if funcOffset < 0 { + return nil, nil, 0, false + } + + if len(origActualExpr.Args) <= funcOffset { + return nil, nil, 0, false + } + + origArgExpr = origActualExpr.Args[funcOffset] + argExprClone = actualExprClone.Args[funcOffset] + + if gomegainfo.IsAsyncActualMethod(actualMethodName) { + if pass.TypesInfo.TypeOf(origArgExpr).String() == "context.Context" { + funcOffset++ + if len(origActualExpr.Args) <= funcOffset { + return nil, nil, 0, false + } + + origArgExpr = origActualExpr.Args[funcOffset] + argExprClone = actualExprClone.Args[funcOffset] + } + } + + return origArgExpr, argExprClone, funcOffset, true +} + +type ArgPayload interface { + ArgType() ArgType +} + +type RegularArgPayload struct { + value.Value +} + +func newRegularArgPayload(orig, clone ast.Expr, pass *analysis.Pass) *RegularArgPayload { + return &RegularArgPayload{ + Value: value.New(orig, clone, pass), + } +} + +func (*RegularArgPayload) ArgType() ArgType { + return UnknownActualArgType +} + +type FuncCallArgPayload struct { + argType ArgType + + origFunc *ast.CallExpr + cloneFunc *ast.CallExpr + + origVal ast.Expr + cloneVal ast.Expr +} + +func newFuncCallArgPayload(orig, clone *ast.CallExpr) ArgPayload { + funcName, ok := builtinFuncName(orig) + if !ok { + return nil + } + + if len(orig.Args) != 1 { + return nil + } + + var argType ArgType + switch funcName { + case "len": + argType = LenFuncActualArgType + case "cap": + argType = CapFuncActualArgType + default: + return nil + } + + return &FuncCallArgPayload{ + argType: argType, + origFunc: orig, + cloneFunc: clone, + origVal: orig.Args[0], + cloneVal: clone.Args[0], + } +} + +func (f *FuncCallArgPayload) ArgType() ArgType { + return f.argType +} + +type ErrPayload struct { + value.Valuer +} + +func newErrPayload(orig, clone ast.Expr, pass *analysis.Pass) *ErrPayload { + return &ErrPayload{ + Valuer: value.GetValuer(orig, clone, pass), + } +} + +func (*ErrPayload) ArgType() ArgType { + return ErrActualArgType | ErrorTypeArgType +} + +func parseBinaryExpr(origActualExpr, argExprClone *ast.BinaryExpr, pass *analysis.Pass) ArgPayload { + left, right, op := origActualExpr.X, origActualExpr.Y, origActualExpr.Op + replace := false + switch realFirst := left.(type) { + case *ast.Ident: // check if const + info, ok := pass.TypesInfo.Types[realFirst] + if ok { + if value.Is[*gotypes.Basic](info.Type) && (info.Value != nil || info.IsNil()) { + replace = true + } + } + + case *ast.BasicLit: + replace = true + } + + if replace { + left, right = right, left + } + + switch op { + case token.EQL: + case token.NEQ: + case token.GTR, token.GEQ, token.LSS, token.LEQ: + if replace { + op = reverseassertion.ChangeCompareOperator(op) + } + default: + return nil + } + + leftClone, rightClone := argExprClone.X, argExprClone.Y + if replace { + leftClone, rightClone = rightClone, leftClone + } + + leftVal := value.GetValuer(left, leftClone, pass) + rightVal := value.GetValuer(right, rightClone, pass) + + if value.IsNil(right, pass) { + return newNilComparisonPayload(leftVal, rightVal, op) + } + + leftVal.IsFunc() + if firstFunc, ok := left.(*ast.CallExpr); ok { + if payload, ok := newFuncComparisonPayload(firstFunc, leftClone.(*ast.CallExpr), right, rightClone, op, pass); ok { + return payload + } + } + + return newComparisonArgPayload(leftVal, rightVal, op) +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/asyncactual.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/asyncactual.go new file mode 100644 index 000000000..7c5df2a34 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/asyncactual.go @@ -0,0 +1,123 @@ +package actual + +import ( + "go/ast" + gotypes "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/intervals" +) + +type AsyncArg struct { + valid bool + fun *ast.CallExpr + + timeoutInterval intervals.DurationValue + pollingInterval intervals.DurationValue + tooManyTimeouts bool + tooManyPolling bool +} + +func newAsyncArg(origExpr, cloneExpr, orig, clone *ast.CallExpr, argType gotypes.Type, pass *analysis.Pass, actualOffset int, timePkg string) *AsyncArg { + var ( + fun *ast.CallExpr + valid = true + timeout intervals.DurationValue + polling intervals.DurationValue + ) + + if _, isActualFuncCall := orig.Args[actualOffset].(*ast.CallExpr); isActualFuncCall { + fun = clone.Args[actualOffset].(*ast.CallExpr) + valid = isValidAsyncValueType(argType) + } + + timeoutOffset := actualOffset + 1 + //var err error + tooManyTimeouts := false + tooManyPolling := false + + if len(orig.Args) > timeoutOffset { + timeout = intervals.GetDuration(pass, timeoutOffset, orig.Args[timeoutOffset], clone.Args[timeoutOffset], timePkg) + pollingOffset := actualOffset + 2 + if len(orig.Args) > pollingOffset { + polling = intervals.GetDuration(pass, pollingOffset, orig.Args[pollingOffset], clone.Args[pollingOffset], timePkg) + } + } + selOrig := origExpr.Fun.(*ast.SelectorExpr) + selClone := cloneExpr.Fun.(*ast.SelectorExpr) + + for { + callOrig, ok := selOrig.X.(*ast.CallExpr) + if !ok { + break + } + callClone := selClone.X.(*ast.CallExpr) + + funOrig, ok := callOrig.Fun.(*ast.SelectorExpr) + if !ok { + break + } + funClone := callClone.Fun.(*ast.SelectorExpr) + + switch funOrig.Sel.Name { + case "WithTimeout", "Within": + if timeout != nil { + tooManyTimeouts = true + } else if len(callOrig.Args) == 1 { + timeout = intervals.GetDurationFromValue(pass, callOrig.Args[0], callClone.Args[0]) + } + + case "WithPolling", "ProbeEvery": + if polling != nil { + tooManyPolling = true + } else if len(callOrig.Args) == 1 { + polling = intervals.GetDurationFromValue(pass, callOrig.Args[0], callClone.Args[0]) + } + } + + selOrig = funOrig + selClone = funClone + } + + return &AsyncArg{ + valid: valid, + fun: fun, + timeoutInterval: timeout, + pollingInterval: polling, + tooManyTimeouts: tooManyTimeouts, + tooManyPolling: tooManyPolling, + } +} + +func (a *AsyncArg) IsValid() bool { + return a.valid +} + +func (a *AsyncArg) Timeout() intervals.DurationValue { + return a.timeoutInterval +} + +func (a *AsyncArg) Polling() intervals.DurationValue { + return a.pollingInterval +} + +func (a *AsyncArg) TooManyTimeouts() bool { + return a.tooManyTimeouts +} + +func (a *AsyncArg) TooManyPolling() bool { + return a.tooManyPolling +} + +func isValidAsyncValueType(t gotypes.Type) bool { + switch t.(type) { + // allow functions that return function or channel. + case *gotypes.Signature, *gotypes.Chan, *gotypes.Pointer: + return true + case *gotypes.Named: + return isValidAsyncValueType(t.Underlying()) + } + + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/asyncfuncarg.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/asyncfuncarg.go new file mode 100644 index 000000000..c777cd4a7 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/asyncfuncarg.go @@ -0,0 +1,38 @@ +package actual + +import ( + gotypes "go/types" + + "github.com/nunnatsa/ginkgolinter/internal/gomegainfo" + "github.com/nunnatsa/ginkgolinter/internal/interfaces" +) + +func getAsyncFuncArg(sig *gotypes.Signature) ArgPayload { + argType := FuncSigArgType + if sig.Results().Len() == 1 { + if interfaces.ImplementsError(sig.Results().At(0).Type().Underlying()) { + argType |= ErrFuncActualArgType | ErrorTypeArgType + } + } + + if sig.Params().Len() > 0 { + arg := sig.Params().At(0).Type() + if gomegainfo.IsGomegaType(arg) && sig.Results().Len() == 0 { + argType |= FuncSigArgType | GomegaParamArgType + } + } + + if sig.Results().Len() > 1 { + argType |= FuncSigArgType | MultiRetsArgType + } + + return &FuncSigArgPayload{argType: argType} +} + +type FuncSigArgPayload struct { + argType ArgType +} + +func (f FuncSigArgPayload) ArgType() ArgType { + return f.argType +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/comparisonAsserion.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/comparisonAsserion.go new file mode 100644 index 000000000..2b16402db --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/actual/comparisonAsserion.go @@ -0,0 +1,260 @@ +package actual + +import ( + "go/ast" + "go/constant" + "go/token" + gotypes "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/expression/value" +) + +type ComparisonActualPayload interface { + GetOp() token.Token + GetLeft() value.Valuer + GetRight() value.Valuer +} + +type FuncComparisonPayload struct { + op token.Token + argType ArgType + val value.Valuer + left value.Valuer + arg ast.Expr +} + +func newFuncComparisonPayload(origLeft, leftClone *ast.CallExpr, origRight, rightClone ast.Expr, op token.Token, pass *analysis.Pass) (*FuncComparisonPayload, bool) { + + funcName, ok := builtinFuncName(origLeft) + if !ok { + return nil, false + } + + if len(origLeft.Args) != 1 { + return nil, false + } + + left := value.GetValuer(origLeft, leftClone, pass) + val := value.GetValuer(origRight, rightClone, pass) + + argType := ComparisonActualArgType + switch funcName { + case "len": + argType |= LenComparisonActualArgType + + if val.IsValueNumeric() { + if val.IsValueZero() { + switch op { + case token.EQL: + argType |= EqualZero + + case token.NEQ, token.GTR: + argType |= GreaterThanZero + } + } else if val.GetValue().String() == "1" && op == token.GEQ { + argType |= GreaterThanZero + } + } + + if !argType.Is(GreaterThanZero) && op != token.EQL && op != token.NEQ { + return nil, false + } + + case "cap": + if op != token.EQL && op != token.NEQ { + return nil, false + } + argType |= CapComparisonActualArgType + + default: + return nil, false + } + + return &FuncComparisonPayload{ + op: op, + argType: argType, + val: val, + left: left, + arg: leftClone.Args[0], + }, true +} + +func (f *FuncComparisonPayload) GetLeft() value.Valuer { + return f.left +} + +func (f *FuncComparisonPayload) GetRight() value.Valuer { + return f.val +} + +func (f *FuncComparisonPayload) ArgType() ArgType { + return f.argType +} + +func (f *FuncComparisonPayload) GetOp() token.Token { + return f.op +} + +func (f *FuncComparisonPayload) GetValue() constant.Value { + return f.val.GetValue() +} + +func (f *FuncComparisonPayload) GetType() gotypes.Type { + return f.val.GetType() +} + +func (f *FuncComparisonPayload) GetValueExpr() ast.Expr { + return f.val.GetValueExpr() +} + +func (f *FuncComparisonPayload) IsError() bool { + return f.val.IsError() +} + +func (f *FuncComparisonPayload) IsValueZero() bool { + return f.val.IsValueZero() +} + +func (f *FuncComparisonPayload) IsFunc() bool { + return true +} + +func (f *FuncComparisonPayload) IsValueNumeric() bool { + return f.val.IsValueNumeric() +} + +func (f *FuncComparisonPayload) IsValueInt() bool { + return f.val.IsValueInt() +} + +func (f *FuncComparisonPayload) IsInterface() bool { + return f.val.IsInterface() +} + +func (f *FuncComparisonPayload) IsPointer() bool { + return f.val.IsPointer() +} + +func (f *FuncComparisonPayload) GetFuncArg() ast.Expr { + return f.arg +} + +type ComparisonArgPayload struct { + left value.Valuer + right value.Valuer + op token.Token +} + +func newComparisonArgPayload(left, right value.Valuer, op token.Token) *ComparisonArgPayload { + return &ComparisonArgPayload{ + left: left, + right: right, + op: op, + } +} + +func (*ComparisonArgPayload) ArgType() ArgType { + return BinaryComparisonActualArgType | ComparisonActualArgType +} + +func (c *ComparisonArgPayload) GetOp() token.Token { + return c.op +} + +func (c *ComparisonArgPayload) GetLeft() value.Valuer { + return c.left +} + +func (c *ComparisonArgPayload) GetRight() value.Valuer { + return c.right +} + +type NilComparisonPayload struct { + val value.Valuer + right value.Valuer + op token.Token +} + +func newNilComparisonPayload(val, right value.Valuer, op token.Token) *NilComparisonPayload { + return &NilComparisonPayload{ + val: val, + right: right, + op: op, + } +} + +func (*NilComparisonPayload) ArgType() ArgType { + return NilComparisonActualArgType +} + +func (n *NilComparisonPayload) GetLeft() value.Valuer { + return n.val +} + +func (n *NilComparisonPayload) GetRight() value.Valuer { + return n.right +} + +func (n *NilComparisonPayload) GetType() gotypes.Type { + return n.val.GetType() +} + +func (n *NilComparisonPayload) GetValue() constant.Value { + return n.val.GetValue() +} + +func (n *NilComparisonPayload) GetValueExpr() ast.Expr { + return n.val.GetValueExpr() +} + +func (n *NilComparisonPayload) IsValueInt() bool { + return n.val.IsValueInt() +} + +func (n *NilComparisonPayload) IsError() bool { + return n.val.IsError() +} + +func (n *NilComparisonPayload) IsValueNumeric() bool { + return n.val.IsValueNumeric() +} + +func (n *NilComparisonPayload) IsFunc() bool { + return n.val.IsFunc() +} + +func (n *NilComparisonPayload) IsValueZero() bool { + return n.val.IsValueZero() +} + +func (n *NilComparisonPayload) IsInterface() bool { + return n.val.IsInterface() +} + +func (n *NilComparisonPayload) IsPointer() bool { + return n.val.IsPointer() +} + +func (n *NilComparisonPayload) GetOp() token.Token { + return n.op +} + +func builtinFuncName(callExpr *ast.CallExpr) (string, bool) { + argFunc, ok := callExpr.Fun.(*ast.Ident) + if !ok { + return "", false + } + + if len(callExpr.Args) != 1 { + return "", false + } + + switch name := argFunc.Name; name { + case "len", "cap", "min", "max": + return name, true + default: + return "", false + } +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/expression.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/expression.go new file mode 100644 index 000000000..976e726fc --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/expression.go @@ -0,0 +1,315 @@ +package expression + +import ( + "fmt" + "go/ast" + "go/token" + gotypes "go/types" + + "github.com/nunnatsa/ginkgolinter/internal/formatter" + + "github.com/go-toolsmith/astcopy" + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/expression/value" + "github.com/nunnatsa/ginkgolinter/internal/gomegahandler" + "github.com/nunnatsa/ginkgolinter/internal/gomegainfo" + "github.com/nunnatsa/ginkgolinter/internal/reverseassertion" +) + +type GomegaExpression struct { + orig *ast.CallExpr + clone *ast.CallExpr + + assertionFuncName string + origAssertionFuncName string + actualFuncName string + + isAsync bool + + actual *actual.Actual + matcher *matcher.Matcher + + handler gomegahandler.Handler +} + +func New(origExpr *ast.CallExpr, pass *analysis.Pass, handler gomegahandler.Handler, timePkg string) (*GomegaExpression, bool) { + actualMethodName, ok := handler.GetActualFuncName(origExpr) + if !ok || !gomegainfo.IsActualMethod(actualMethodName) { + return nil, false + } + + origSel, ok := origExpr.Fun.(*ast.SelectorExpr) + if !ok || !gomegainfo.IsAssertionFunc(origSel.Sel.Name) { + return &GomegaExpression{ + orig: origExpr, + actualFuncName: actualMethodName, + }, true + } + + exprClone := astcopy.CallExpr(origExpr) + selClone := exprClone.Fun.(*ast.SelectorExpr) + + origActual := handler.GetActualExpr(origSel) + if origActual == nil { + return nil, false + } + + actualClone := handler.GetActualExprClone(origSel, selClone) + if actualClone == nil { + return nil, false + } + + actl, ok := actual.New(origExpr, exprClone, origActual, actualClone, pass, handler, timePkg) + if !ok { + return nil, false + } + + origMatcher, ok := origExpr.Args[0].(*ast.CallExpr) + if !ok { + return nil, false + } + + matcherClone := exprClone.Args[0].(*ast.CallExpr) + + mtchr, ok := matcher.New(origMatcher, matcherClone, pass, handler) + if !ok { + return nil, false + } + + exprClone.Args[0] = mtchr.Clone + + gexp := &GomegaExpression{ + orig: origExpr, + clone: exprClone, + + assertionFuncName: origSel.Sel.Name, + origAssertionFuncName: origSel.Sel.Name, + actualFuncName: actualMethodName, + + isAsync: actl.IsAsync(), + + actual: actl, + matcher: mtchr, + + handler: handler, + } + + if mtchr.ShouldReverseLogic() { + gexp.ReverseAssertionFuncLogic() + } + + return gexp, true +} + +func (e *GomegaExpression) IsMissingAssertion() bool { + return e.matcher == nil +} + +func (e *GomegaExpression) GetActualFuncName() string { + if e == nil { + return "" + } + return e.actualFuncName +} + +func (e *GomegaExpression) GetAssertFuncName() string { + if e == nil { + return "" + } + return e.assertionFuncName +} + +func (e *GomegaExpression) GetOrigAssertFuncName() string { + if e == nil { + return "" + } + return e.origAssertionFuncName +} + +func (e *GomegaExpression) IsAsync() bool { + return e.isAsync +} + +func (e *GomegaExpression) ReverseAssertionFuncLogic() { + assertionFunc := e.clone.Fun.(*ast.SelectorExpr).Sel + newName := reverseassertion.ChangeAssertionLogic(assertionFunc.Name) + assertionFunc.Name = newName + e.assertionFuncName = newName +} + +func (e *GomegaExpression) ReplaceAssertionMethod(name string) { + e.clone.Fun.(*ast.SelectorExpr).Sel.Name = name +} + +func (e *GomegaExpression) ReplaceMatcherFuncName(name string) { + e.matcher.ReplaceMatcherFuncName(name) +} + +func (e *GomegaExpression) ReplaceMatcherArgs(newArgs []ast.Expr) { + e.matcher.ReplaceMatcherArgs(newArgs) +} + +func (e *GomegaExpression) RemoveMatcherArgs() { + e.matcher.ReplaceMatcherArgs(nil) +} + +func (e *GomegaExpression) ReplaceActual(newArg ast.Expr) { + e.actual.ReplaceActual(newArg) +} + +func (e *GomegaExpression) ReplaceActualWithItsFirstArg() { + e.actual.ReplaceActualWithItsFirstArg() +} + +func (e *GomegaExpression) replaceMathcerFuncNoArgs(name string) { + e.matcher.ReplaceMatcherFuncName(name) + e.RemoveMatcherArgs() +} + +func (e *GomegaExpression) SetMatcherBeZero() { + e.replaceMathcerFuncNoArgs("BeZero") +} + +func (e *GomegaExpression) SetMatcherBeEmpty() { + e.replaceMathcerFuncNoArgs("BeEmpty") +} + +func (e *GomegaExpression) SetLenNumericMatcher() { + if m, ok := e.matcher.GetMatcherInfo().(value.Valuer); ok && m.IsValueZero() { + e.SetMatcherBeEmpty() + } else { + e.ReplaceMatcherFuncName("HaveLen") + e.ReplaceMatcherArgs([]ast.Expr{m.GetValueExpr()}) + } +} + +func (e *GomegaExpression) SetLenNumericActual() { + if m, ok := e.matcher.GetMatcherInfo().(value.Valuer); ok && m.IsValueZero() { + e.SetMatcherBeEmpty() + } else { + e.ReplaceMatcherFuncName("HaveLen") + e.ReplaceMatcherArgs([]ast.Expr{m.GetValueExpr()}) + } +} + +func (e *GomegaExpression) SetMatcherLen(arg ast.Expr) { + e.ReplaceMatcherFuncName("HaveLen") + e.ReplaceMatcherArgs([]ast.Expr{arg}) +} + +func (e *GomegaExpression) SetMatcherCap(arg ast.Expr) { + e.ReplaceMatcherFuncName("HaveCap") + e.ReplaceMatcherArgs([]ast.Expr{arg}) +} + +func (e *GomegaExpression) SetMatcherCapZero() { + e.ReplaceMatcherFuncName("HaveCap") + e.ReplaceMatcherArgs([]ast.Expr{&ast.BasicLit{Kind: token.INT, Value: "0"}}) +} + +func (e *GomegaExpression) SetMatcherSucceed() { + e.replaceMathcerFuncNoArgs("Succeed") +} + +func (e *GomegaExpression) SetMatcherHaveOccurred() { + e.replaceMathcerFuncNoArgs("HaveOccurred") +} + +func (e *GomegaExpression) SetMatcherBeNil() { + e.replaceMathcerFuncNoArgs("BeNil") +} + +func (e *GomegaExpression) SetMatcherBeTrue() { + e.replaceMathcerFuncNoArgs("BeTrue") +} + +func (e *GomegaExpression) SetMatcherBeFalse() { + e.replaceMathcerFuncNoArgs("BeFalse") +} + +func (e *GomegaExpression) SetMatcherHaveValue() { + newMatcherExp := e.handler.GetNewWrapperMatcher("HaveValue", e.matcher.Clone) + e.clone.Args[0] = newMatcherExp + e.matcher.Clone = newMatcherExp +} + +func (e *GomegaExpression) SetMatcherEqual(arg ast.Expr) { + e.ReplaceMatcherFuncName("Equal") + e.ReplaceMatcherArgs([]ast.Expr{arg}) +} + +func (e *GomegaExpression) SetMatcherBeIdenticalTo(arg ast.Expr) { + e.ReplaceMatcherFuncName("BeIdenticalTo") + e.ReplaceMatcherArgs([]ast.Expr{arg}) +} + +func (e *GomegaExpression) SetMatcherBeNumerically(op token.Token, arg ast.Expr) { + e.ReplaceMatcherFuncName("BeNumerically") + e.ReplaceMatcherArgs([]ast.Expr{ + &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", op.String())}, + arg, + }) +} + +func (e *GomegaExpression) IsNegativeAssertion() bool { + return reverseassertion.IsNegativeLogic(e.assertionFuncName) +} + +func (e *GomegaExpression) GetClone() *ast.CallExpr { + return e.clone +} + +// Actual proxies: + +func (e *GomegaExpression) GetActualClone() *ast.CallExpr { + return e.actual.Clone +} + +func (e *GomegaExpression) AppendWithArgsToActual() { + e.actual.AppendWithArgsMethod() +} + +func (e *GomegaExpression) GetAsyncActualArg() *actual.AsyncArg { + return e.actual.GetAsyncArg() +} + +func (e *GomegaExpression) GetActualArg() actual.ArgPayload { + return e.actual.Arg +} + +func (e *GomegaExpression) GetActualArgExpr() ast.Expr { + return e.actual.GetActualArg() +} + +func (e *GomegaExpression) GetActualArgGOType() gotypes.Type { + return e.actual.ArgGOType() +} + +func (e *GomegaExpression) ActualArgTypeIs(other actual.ArgType) bool { + return e.actual.Arg.ArgType().Is(other) +} + +func (e *GomegaExpression) IsActualTuple() bool { + return e.actual.IsTuple() +} + +// Matcher proxies + +func (e *GomegaExpression) GetMatcher() *matcher.Matcher { + return e.matcher +} + +func (e *GomegaExpression) GetMatcherInfo() matcher.Info { + return e.matcher.GetMatcherInfo() +} + +func (e *GomegaExpression) MatcherTypeIs(other matcher.Type) bool { + return e.matcher.GetMatcherInfo().Type().Is(other) +} + +func (e *GomegaExpression) FormatOrig(frm *formatter.GoFmtFormatter) string { + return frm.Format(e.orig) +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/bematchers.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/bematchers.go new file mode 100644 index 000000000..24272535d --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/bematchers.go @@ -0,0 +1,77 @@ +package matcher + +import "github.com/nunnatsa/ginkgolinter/internal/expression/value" + +type BeIdenticalToMatcher struct { + value.Value +} + +func (BeIdenticalToMatcher) Type() Type { + return BeIdenticalToMatcherType +} + +func (BeIdenticalToMatcher) MatcherName() string { + return beIdenticalTo +} + +type BeEquivalentToMatcher struct { + value.Value +} + +func (BeEquivalentToMatcher) Type() Type { + return BeEquivalentToMatcherType +} + +func (BeEquivalentToMatcher) MatcherName() string { + return beEquivalentTo +} + +type BeZeroMatcher struct{} + +func (BeZeroMatcher) Type() Type { + return BeZeroMatcherType +} + +func (BeZeroMatcher) MatcherName() string { + return beZero +} + +type BeEmptyMatcher struct{} + +func (BeEmptyMatcher) Type() Type { + return BeEmptyMatcherType +} + +func (BeEmptyMatcher) MatcherName() string { + return beEmpty +} + +type BeTrueMatcher struct{} + +func (BeTrueMatcher) Type() Type { + return BeTrueMatcherType | BoolValueTrue +} + +func (BeTrueMatcher) MatcherName() string { + return beTrue +} + +type BeFalseMatcher struct{} + +func (BeFalseMatcher) Type() Type { + return BeFalseMatcherType | BoolValueFalse +} + +func (BeFalseMatcher) MatcherName() string { + return beFalse +} + +type BeNilMatcher struct{} + +func (BeNilMatcher) Type() Type { + return BeNilMatcherType +} + +func (BeNilMatcher) MatcherName() string { + return beNil +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/benumericmatcher.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/benumericmatcher.go new file mode 100644 index 000000000..8683f0291 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/benumericmatcher.go @@ -0,0 +1,128 @@ +package matcher + +import ( + "go/ast" + "go/constant" + "go/token" + gotypes "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/expression/value" +) + +type BeNumericallyMatcher struct { + op token.Token + value value.Valuer + argType Type +} + +var compareOps = map[string]token.Token{ + `"=="`: token.EQL, + `"<"`: token.LSS, + `">"`: token.GTR, + `"="`: token.ASSIGN, + `"!="`: token.NEQ, + `"<="`: token.LEQ, + `">="`: token.GEQ, +} + +func getCompareOp(opExp ast.Expr) token.Token { + basic, ok := opExp.(*ast.BasicLit) + if !ok { + return token.ILLEGAL + } + if basic.Kind != token.STRING { + return token.ILLEGAL + } + + if tk, ok := compareOps[basic.Value]; ok { + return tk + } + + return token.ILLEGAL +} + +func newBeNumericallyMatcher(opExp, orig, clone ast.Expr, pass *analysis.Pass) Info { + op := getCompareOp(opExp) + if op == token.ILLEGAL { + return &UnspecifiedMatcher{ + matcherName: beNumerically, + } + } + + val := value.GetValuer(orig, clone, pass) + argType := BeNumericallyMatcherType + + if val.IsValueNumeric() { + if v := val.GetValue().String(); v == "0" { + switch op { + case token.EQL: + argType |= EqualZero + + case token.NEQ, token.GTR: + argType |= GreaterThanZero + } + } else if v == "1" && op == token.GEQ { + argType |= GreaterThanZero + } + } + + return &BeNumericallyMatcher{ + op: op, + value: val, + argType: argType, + } +} + +func (m BeNumericallyMatcher) Type() Type { + return m.argType +} + +func (BeNumericallyMatcher) MatcherName() string { + return beNumerically +} + +func (m BeNumericallyMatcher) GetValueExpr() ast.Expr { + return m.value.GetValueExpr() +} + +func (m BeNumericallyMatcher) GetValue() constant.Value { + return m.value.GetValue() +} + +func (m BeNumericallyMatcher) GetType() gotypes.Type { + return m.value.GetType() +} + +func (m BeNumericallyMatcher) GetOp() token.Token { + return m.op +} + +func (m BeNumericallyMatcher) IsValueZero() bool { + return m.value.IsValueZero() +} + +func (m BeNumericallyMatcher) IsValueInt() bool { + return m.value.IsValueInt() +} + +func (m BeNumericallyMatcher) IsValueNumeric() bool { + return m.value.IsValueNumeric() +} + +func (m BeNumericallyMatcher) IsError() bool { + return m.value.IsError() +} + +func (m BeNumericallyMatcher) IsFunc() bool { + return m.value.IsFunc() +} + +func (m BeNumericallyMatcher) IsInterface() bool { + return m.value.IsInterface() +} + +func (m BeNumericallyMatcher) IsPointer() bool { + return m.value.IsPointer() +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/equalmatcher.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/equalmatcher.go new file mode 100644 index 000000000..8cee8e408 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/equalmatcher.go @@ -0,0 +1,124 @@ +package matcher + +import ( + "go/ast" + "go/constant" + gotypes "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/expression/value" +) + +func newEqualMatcher(orig, clone ast.Expr, pass *analysis.Pass) Info { + t := pass.TypesInfo.Types[orig] + + if t.Value != nil { + if t.Value.Kind() == constant.Bool { + if t.Value.String() == "true" { + return &EqualTrueMatcher{} + } + return &EqualFalseMatcher{} + } + } + + if value.IsNil(orig, pass) { + return &EqualNilMatcher{ + gotype: pass.TypesInfo.TypeOf(orig), + } + } + + val := value.GetValuer(orig, clone, pass) + + return &EqualMatcher{ + val: val, + } +} + +type EqualMatcher struct { + val value.Valuer +} + +func (EqualMatcher) Type() Type { + return EqualMatcherType +} + +func (EqualMatcher) MatcherName() string { + return equal +} + +func (m EqualMatcher) GetValue() constant.Value { + return m.val.GetValue() +} + +func (m EqualMatcher) GetType() gotypes.Type { + return m.val.GetType() +} + +func (m EqualMatcher) GetValueExpr() ast.Expr { + return m.val.GetValueExpr() +} + +func (m EqualMatcher) IsValueZero() bool { + return m.val.IsValueZero() +} + +func (m EqualMatcher) IsValueInt() bool { + return m.val.IsValueInt() +} + +func (m EqualMatcher) IsValueNumeric() bool { + return m.val.IsValueNumeric() +} + +func (m EqualMatcher) IsError() bool { + return m.val.IsError() +} + +func (m EqualMatcher) IsFunc() bool { + return m.val.IsFunc() +} + +func (m EqualMatcher) IsInterface() bool { + return m.val.IsInterface() +} + +func (m EqualMatcher) IsPointer() bool { + return m.val.IsPointer() +} + +type EqualNilMatcher struct { + gotype gotypes.Type +} + +func (EqualNilMatcher) Type() Type { + return EqualNilMatcherType | EqualMatcherType | EqualValueMatcherType +} + +func (EqualNilMatcher) MatcherName() string { + return equal +} + +func (n EqualNilMatcher) GetType() gotypes.Type { + return n.gotype +} + +type EqualTrueMatcher struct{} + +func (EqualTrueMatcher) Type() Type { + return EqualMatcherType | EqualBoolValueMatcherType | BoolValueTrue +} + +func (EqualTrueMatcher) MatcherName() string { + return equal +} + +type EqualFalseMatcher struct{} + +func (EqualFalseMatcher) Type() Type { + return EqualMatcherType | EqualBoolValueMatcherType | BoolValueFalse +} + +func (EqualFalseMatcher) MatcherName() string { + return equal +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/errormatchers.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/errormatchers.go new file mode 100644 index 000000000..a493287e0 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/errormatchers.go @@ -0,0 +1,199 @@ +package matcher + +import ( + "go/ast" + gotypes "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/expression/value" + "github.com/nunnatsa/ginkgolinter/internal/interfaces" +) + +type HaveOccurredMatcher struct{} + +func (m *HaveOccurredMatcher) Type() Type { + return HaveOccurredMatcherType +} +func (m *HaveOccurredMatcher) MatcherName() string { + return haveOccurred +} + +type SucceedMatcher struct{} + +func (m *SucceedMatcher) Type() Type { + return SucceedMatcherType +} +func (m *SucceedMatcher) MatcherName() string { + return succeed +} + +type MatchErrorMatcher interface { + Info + AllowedNumArgs() int + NumArgs() int +} + +type InvalidMatchErrorMatcher struct { + firstAgr ast.Expr + numArgs int +} + +func (m *InvalidMatchErrorMatcher) Type() Type { + return MatchErrorMatcherType +} + +func (m *InvalidMatchErrorMatcher) MatcherName() string { + return matchError +} + +func (m *InvalidMatchErrorMatcher) AllowedNumArgs() int { + return 1 +} + +func (m *InvalidMatchErrorMatcher) NumArgs() int { + return m.numArgs +} + +func (m *InvalidMatchErrorMatcher) GetValueExpr() ast.Expr { + return m.firstAgr +} + +type MatchErrorMatcherWithErr struct { + numArgs int +} + +func (m *MatchErrorMatcherWithErr) Type() Type { + return MatchErrorMatcherType | ErrMatchWithErr +} + +func (m *MatchErrorMatcherWithErr) MatcherName() string { + return matchError +} + +func (m *MatchErrorMatcherWithErr) AllowedNumArgs() int { + return 1 +} + +func (m *MatchErrorMatcherWithErr) NumArgs() int { + return m.numArgs +} + +type MatchErrorMatcherWithErrFunc struct { + numArgs int + secondArgIsString bool +} + +func (m *MatchErrorMatcherWithErrFunc) Type() Type { + return MatchErrorMatcherType | ErrMatchWithErrFunc +} + +func (m *MatchErrorMatcherWithErrFunc) MatcherName() string { + return matchError +} + +func (m *MatchErrorMatcherWithErrFunc) AllowedNumArgs() int { + return 2 +} + +func (m *MatchErrorMatcherWithErrFunc) NumArgs() int { + return m.numArgs +} + +func (m *MatchErrorMatcherWithErrFunc) IsSecondArgString() bool { + return m.secondArgIsString +} + +type MatchErrorMatcherWithString struct { + numArgs int +} + +func (m *MatchErrorMatcherWithString) Type() Type { + return MatchErrorMatcherType | ErrMatchWithString +} + +func (m *MatchErrorMatcherWithString) MatcherName() string { + return matchError +} + +func (m *MatchErrorMatcherWithString) AllowedNumArgs() int { + return 1 +} + +func (m *MatchErrorMatcherWithString) NumArgs() int { + return m.numArgs +} + +type MatchErrorMatcherWithMatcher struct { + numArgs int +} + +func (m *MatchErrorMatcherWithMatcher) Type() Type { + return MatchErrorMatcherType | ErrMatchWithMatcher +} + +func (m *MatchErrorMatcherWithMatcher) MatcherName() string { + return matchError +} + +func (m *MatchErrorMatcherWithMatcher) AllowedNumArgs() int { + return 1 +} + +func (m *MatchErrorMatcherWithMatcher) NumArgs() int { + return m.numArgs +} + +func newMatchErrorMatcher(args []ast.Expr, pass *analysis.Pass) MatchErrorMatcher { + numArgs := len(args) + if value.IsExprError(pass, args[0]) { + return &MatchErrorMatcherWithErr{numArgs: numArgs} + } + + t := pass.TypesInfo.TypeOf(args[0]) + if isString(args[0], pass) { + return &MatchErrorMatcherWithString{numArgs: numArgs} + } + + if interfaces.ImplementsGomegaMatcher(t) { + return &MatchErrorMatcherWithMatcher{numArgs: numArgs} + } + + if isFuncErrBool(t) { + isString := false + if numArgs > 1 { + t2 := pass.TypesInfo.TypeOf(args[1]) + isString = gotypes.Identical(t2, gotypes.Typ[gotypes.String]) + } + return &MatchErrorMatcherWithErrFunc{numArgs: numArgs, secondArgIsString: isString} + } + + return &InvalidMatchErrorMatcher{numArgs: numArgs} +} + +func isString(exp ast.Expr, pass *analysis.Pass) bool { + t := pass.TypesInfo.TypeOf(exp) + return gotypes.Identical(t, gotypes.Typ[gotypes.String]) +} + +// isFuncErrBool checks if a function is with the signature `func(error) bool` +func isFuncErrBool(t gotypes.Type) bool { + sig, ok := t.(*gotypes.Signature) + if !ok { + return false + } + if sig.Params().Len() != 1 || sig.Results().Len() != 1 { + return false + } + + if !interfaces.ImplementsError(sig.Params().At(0).Type()) { + return false + } + + b, ok := sig.Results().At(0).Type().(*gotypes.Basic) + if ok && b.Name() == "bool" && b.Info() == gotypes.IsBoolean && b.Kind() == gotypes.Bool { + return true + } + + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/lenmatchers.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/lenmatchers.go new file mode 100644 index 000000000..8e4f438e8 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/lenmatchers.go @@ -0,0 +1,11 @@ +package matcher + +type HaveLenZeroMatcher struct{} + +func (HaveLenZeroMatcher) Type() Type { + return HaveLenZeroMatcherType +} + +func (HaveLenZeroMatcher) MatcherName() string { + return haveLen +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcher.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcher.go new file mode 100644 index 000000000..0969b9551 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcher.go @@ -0,0 +1,86 @@ +package matcher + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/gomegahandler" +) + +const ( // gomega matchers + beEmpty = "BeEmpty" + beEquivalentTo = "BeEquivalentTo" + beFalse = "BeFalse" + beIdenticalTo = "BeIdenticalTo" + beNil = "BeNil" + beNumerically = "BeNumerically" + beTrue = "BeTrue" + beZero = "BeZero" + equal = "Equal" + haveLen = "HaveLen" + haveValue = "HaveValue" + and = "And" + or = "Or" + withTransform = "WithTransform" + matchError = "MatchError" + haveOccurred = "HaveOccurred" + succeed = "Succeed" +) + +type Matcher struct { + funcName string + Orig *ast.CallExpr + Clone *ast.CallExpr + info Info + reverseLogic bool + handler gomegahandler.Handler +} + +func New(origMatcher, matcherClone *ast.CallExpr, pass *analysis.Pass, handler gomegahandler.Handler) (*Matcher, bool) { + reverse := false + var assertFuncName string + for { + ok := false + assertFuncName, ok = handler.GetActualFuncName(origMatcher) + if !ok { + return nil, false + } + + if assertFuncName != "Not" { + break + } + + reverse = !reverse + origMatcher, ok = origMatcher.Args[0].(*ast.CallExpr) + if !ok { + return nil, false + } + matcherClone = matcherClone.Args[0].(*ast.CallExpr) + } + + return &Matcher{ + funcName: assertFuncName, + Orig: origMatcher, + Clone: matcherClone, + info: getMatcherInfo(origMatcher, matcherClone, assertFuncName, pass, handler), + reverseLogic: reverse, + handler: handler, + }, true +} + +func (m *Matcher) ShouldReverseLogic() bool { + return m.reverseLogic +} + +func (m *Matcher) GetMatcherInfo() Info { + return m.info +} + +func (m *Matcher) ReplaceMatcherFuncName(name string) { + m.handler.ReplaceFunction(m.Clone, ast.NewIdent(name)) +} + +func (m *Matcher) ReplaceMatcherArgs(newArgs []ast.Expr) { + m.Clone.Args = newArgs +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcherinfo.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcherinfo.go new file mode 100644 index 000000000..084226bcc --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcherinfo.go @@ -0,0 +1,148 @@ +package matcher + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/expression/value" + "github.com/nunnatsa/ginkgolinter/internal/gomegahandler" +) + +type Type uint64 + +const ( + Unspecified Type = 1 << iota + EqualMatcherType + BeZeroMatcherType + BeEmptyMatcherType + BeTrueMatcherType + BeFalseMatcherType + BeNumericallyMatcherType + HaveLenZeroMatcherType + BeEquivalentToMatcherType + BeIdenticalToMatcherType + BeNilMatcherType + MatchErrorMatcherType + MultipleMatcherMatherType + HaveValueMatherType + WithTransformMatherType + EqualBoolValueMatcherType + EqualValueMatcherType + HaveOccurredMatcherType + SucceedMatcherType + EqualNilMatcherType + + BoolValueFalse + BoolValueTrue + + OrMatherType + AndMatherType + + ErrMatchWithErr + ErrMatchWithErrFunc + ErrMatchWithString + ErrMatchWithMatcher + + EqualZero + GreaterThanZero +) + +type Info interface { + Type() Type + MatcherName() string +} + +func getMatcherInfo(orig, clone *ast.CallExpr, matcherName string, pass *analysis.Pass, handler gomegahandler.Handler) Info { + switch matcherName { + case equal: + return newEqualMatcher(orig.Args[0], clone.Args[0], pass) + + case beZero: + return &BeZeroMatcher{} + + case beEmpty: + return &BeEmptyMatcher{} + + case beTrue: + return &BeTrueMatcher{} + + case beFalse: + return &BeFalseMatcher{} + + case beNil: + return &BeNilMatcher{} + + case beNumerically: + if len(orig.Args) == 2 { + return newBeNumericallyMatcher(orig.Args[0], orig.Args[1], clone.Args[1], pass) + } + + case haveLen: + if value.GetValuer(orig.Args[0], clone.Args[0], pass).IsValueZero() { + return &HaveLenZeroMatcher{} + } + + case beEquivalentTo: + return &BeEquivalentToMatcher{ + Value: value.New(orig.Args[0], clone.Args[0], pass), + } + + case beIdenticalTo: + return &BeIdenticalToMatcher{ + Value: value.New(orig.Args[0], clone.Args[0], pass), + } + + case matchError: + return newMatchErrorMatcher(orig.Args, pass) + + case haveValue: + if nestedMatcher, ok := getNestedMatcher(orig, clone, 0, pass, handler); ok { + return &HaveValueMatcher{ + nested: nestedMatcher, + } + } + + case withTransform: + if nestedMatcher, ok := getNestedMatcher(orig, clone, 1, pass, handler); ok { + return newWithTransformMatcher(orig.Args[0], nestedMatcher, pass) + } + + case or, and: + matcherType := MultipleMatcherMatherType + if matcherName == or { + matcherType |= OrMatherType + } else { + matcherType |= AndMatherType + } + + if m, ok := newMultipleMatchersMatcher(matcherType, orig.Args, clone.Args, pass, handler); ok { + return m + } + + case succeed: + return &SucceedMatcher{} + + case haveOccurred: + return &HaveOccurredMatcher{} + + } + + return &UnspecifiedMatcher{matcherName: matcherName} +} + +type UnspecifiedMatcher struct { + matcherName string +} + +func (UnspecifiedMatcher) Type() Type { + return Unspecified +} + +func (u UnspecifiedMatcher) MatcherName() string { + return u.matcherName +} + +func (t Type) Is(other Type) bool { + return t&other != 0 +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcherwithnest.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcherwithnest.go new file mode 100644 index 000000000..cc26e5ac2 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/matcherwithnest.go @@ -0,0 +1,66 @@ +package matcher + +import ( + "go/ast" + gotypes "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/gomegahandler" +) + +type HaveValueMatcher struct { + nested *Matcher +} + +func (m *HaveValueMatcher) Type() Type { + return HaveValueMatherType +} +func (m *HaveValueMatcher) MatcherName() string { + return haveValue +} + +func (m *HaveValueMatcher) GetNested() *Matcher { + return m.nested +} + +type WithTransformMatcher struct { + funcType gotypes.Type + nested *Matcher +} + +func (m *WithTransformMatcher) Type() Type { + return WithTransformMatherType +} +func (m *WithTransformMatcher) MatcherName() string { + return withTransform +} + +func (m *WithTransformMatcher) GetNested() *Matcher { + return m.nested +} + +func (m *WithTransformMatcher) GetFuncType() gotypes.Type { + return m.funcType +} + +func getNestedMatcher(orig, clone *ast.CallExpr, offset int, pass *analysis.Pass, handler gomegahandler.Handler) (*Matcher, bool) { + if origNested, ok := orig.Args[offset].(*ast.CallExpr); ok { + cloneNested := clone.Args[offset].(*ast.CallExpr) + + return New(origNested, cloneNested, pass, handler) + } + + return nil, false +} + +func newWithTransformMatcher(fun ast.Expr, nested *Matcher, pass *analysis.Pass) *WithTransformMatcher { + funcType := pass.TypesInfo.TypeOf(fun) + if sig, ok := funcType.(*gotypes.Signature); ok && sig.Results().Len() > 0 { + funcType = sig.Results().At(0).Type() + } + return &WithTransformMatcher{ + funcType: funcType, + nested: nested, + } +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/multiplematchers.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/multiplematchers.go new file mode 100644 index 000000000..9ce0cf5b8 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/matcher/multiplematchers.go @@ -0,0 +1,62 @@ +package matcher + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/gomegahandler" +) + +type MultipleMatchersMatcher struct { + matherType Type + matchers []*Matcher +} + +func (m *MultipleMatchersMatcher) Type() Type { + return m.matherType +} + +func (m *MultipleMatchersMatcher) MatcherName() string { + if m.matherType.Is(OrMatherType) { + return or + } + return and +} + +func newMultipleMatchersMatcher(matherType Type, orig, clone []ast.Expr, pass *analysis.Pass, handler gomegahandler.Handler) (*MultipleMatchersMatcher, bool) { + matchers := make([]*Matcher, len(orig)) + + for i := range orig { + nestedOrig, ok := orig[i].(*ast.CallExpr) + if !ok { + return nil, false + } + + m, ok := New(nestedOrig, clone[i].(*ast.CallExpr), pass, handler) + if !ok { + return nil, false + } + + m.reverseLogic = false + + matchers[i] = m + } + + return &MultipleMatchersMatcher{ + matherType: matherType, + matchers: matchers, + }, true +} + +func (m *MultipleMatchersMatcher) Len() int { + return len(m.matchers) +} + +func (m *MultipleMatchersMatcher) At(i int) *Matcher { + if i >= len(m.matchers) { + panic("index out of range") + } + + return m.matchers[i] +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/value/value.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/value/value.go new file mode 100644 index 000000000..dda0dd73b --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/expression/value/value.go @@ -0,0 +1,221 @@ +package value + +import ( + "go/ast" + "go/constant" + gotypes "go/types" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/interfaces" +) + +type Valuer interface { + GetValue() constant.Value + GetType() gotypes.Type + GetValueExpr() ast.Expr + IsValueZero() bool + IsValueInt() bool + IsValueNumeric() bool + IsError() bool + IsFunc() bool + IsInterface() bool + IsPointer() bool +} + +func GetValuer(orig, clone ast.Expr, pass *analysis.Pass) Valuer { + val := New(orig, clone, pass) + unspecified := UnspecifiedValue{ + Value: val, + } + + if orig == nil { + return unspecified + } + + if IsExprError(pass, orig) { + return &ErrValue{ + Value: val, + err: clone, + } + } + + if val.GetValue() == nil || !val.tv.IsValue() { + return unspecified + } + + if val.GetValue().Kind() == constant.Int { + num, ok := constant.Int64Val(val.GetValue()) + if !ok { + return unspecified + } + return &IntValue{ + Value: val, + val: num, + } + } + + return unspecified +} + +type Value struct { + expr ast.Expr + tv gotypes.TypeAndValue +} + +func New(orig, clone ast.Expr, pass *analysis.Pass) Value { + tv := pass.TypesInfo.Types[orig] + + return Value{ + expr: clone, + tv: tv, + } +} + +func (v Value) GetValueExpr() ast.Expr { + return v.expr +} + +func (v Value) GetValue() constant.Value { + return v.tv.Value +} + +func (v Value) GetType() gotypes.Type { + return v.tv.Type +} + +func (v Value) IsInterface() bool { + return gotypes.IsInterface(v.tv.Type) +} + +func (v Value) IsPointer() bool { + return Is[*gotypes.Pointer](v.tv.Type) +} + +func (v Value) IsNil() bool { + return v.tv.IsNil() +} + +type UnspecifiedValue struct { + Value +} + +func (u UnspecifiedValue) IsValueZero() bool { + return false +} + +func (u UnspecifiedValue) IsValueInt() bool { + return false +} + +func (u UnspecifiedValue) IsValueNumeric() bool { + return false +} + +func (u UnspecifiedValue) IsError() bool { + return false +} + +func (u UnspecifiedValue) IsFunc() bool { + return isFunc(u.GetValueExpr()) +} + +type ErrValue struct { + Value + err ast.Expr +} + +func (e ErrValue) IsValueZero() bool { + return false +} + +func (e ErrValue) IsValueInt() bool { + return false +} + +func (e ErrValue) IsValueNumeric() bool { + return false +} + +func (e ErrValue) IsError() bool { + return true +} + +func (e ErrValue) IsFunc() bool { + return isFunc(e.GetValueExpr()) +} + +type IntValuer interface { + GetIntValue() int64 +} + +type IntValue struct { + Value + val int64 +} + +func (i IntValue) IsValueZero() bool { + return i.val == 0 +} + +func (i IntValue) IsValueInt() bool { + return i.val == 0 +} + +func (i IntValue) IsValueNumeric() bool { + return true +} + +func (i IntValue) IsError() bool { + return false +} + +func (i IntValue) IsFunc() bool { + return false +} + +func (i IntValue) GetIntValue() int64 { + return i.val +} + +func isFunc(exp ast.Expr) bool { + return Is[*ast.CallExpr](exp) +} + +func Is[T any](x any) bool { + _, matchType := x.(T) + return matchType +} + +func IsExprError(pass *analysis.Pass, expr ast.Expr) bool { + actualArgType := pass.TypesInfo.TypeOf(expr) + switch t := actualArgType.(type) { + case *gotypes.Named: + return interfaces.ImplementsError(actualArgType) + + case *gotypes.Pointer: + if tt, ok := t.Elem().(*gotypes.Named); ok { + return interfaces.ImplementsError(tt) + } + + case *gotypes.Tuple: + if t.Len() > 0 { + switch t0 := t.At(0).Type().(type) { + case *gotypes.Named, *gotypes.Pointer: + if interfaces.ImplementsError(t0) { + return true + } + } + } + } + return false +} + +func IsNil(exp ast.Expr, pass *analysis.Pass) bool { + id, ok := exp.(*ast.Ident) + if !ok { + return false + } + + return pass.TypesInfo.Types[id].IsNil() +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/formatter/formatter.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/formatter/formatter.go new file mode 100644 index 000000000..64f3d99ad --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/formatter/formatter.go @@ -0,0 +1,22 @@ +package formatter + +import ( + "bytes" + "go/ast" + "go/printer" + "go/token" +) + +type GoFmtFormatter struct { + fset *token.FileSet +} + +func NewGoFmtFormatter(fset *token.FileSet) *GoFmtFormatter { + return &GoFmtFormatter{fset: fset} +} + +func (f GoFmtFormatter) Format(exp ast.Expr) string { + var buf bytes.Buffer + _ = printer.Fprint(&buf, f.fset, exp) + return buf.String() +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/dothandler.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/dothandler.go new file mode 100644 index 000000000..9c54b4334 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/dothandler.go @@ -0,0 +1,36 @@ +package ginkgohandler + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/types" +) + +// dotHandler is used when importing ginkgo with dot; i.e. +// import . "github.com/onsi/ginkgo" +type dotHandler struct{} + +func (h dotHandler) HandleGinkgoSpecs(expr ast.Expr, config types.Config, pass *analysis.Pass) bool { + return handleGinkgoSpecs(expr, config, pass, h) +} + +func (h dotHandler) getFocusContainerName(exp *ast.CallExpr) (bool, *ast.Ident) { + if fun, ok := exp.Fun.(*ast.Ident); ok { + return isFocusContainer(fun.Name), fun + } + return false, nil +} + +func (h dotHandler) isWrapContainer(exp *ast.CallExpr) bool { + if fun, ok := exp.Fun.(*ast.Ident); ok { + return isWrapContainer(fun.Name) + } + return false +} + +func (h dotHandler) isFocusSpec(exp ast.Expr) bool { + id, ok := exp.(*ast.Ident) + return ok && id.Name == focusSpec +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/ginkgoinfo.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/ginkgoinfo.go new file mode 100644 index 000000000..d8bb75399 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/ginkgoinfo.go @@ -0,0 +1,63 @@ +package ginkgohandler + +const ( // container names + describe = "Describe" + pdescribe = "PDescribe" + xdescribe = "XDescribe" + fdescribe = "FDescribe" + + when = "When" + pwhen = "PWhen" + xwhen = "XWhen" + fwhen = "FWhen" + + contextContainer = "Context" + pcontext = "PContext" + xcontext = "XContext" + fcontext = "FContext" + + it = "It" + pit = "PIt" + xit = "XIt" + fit = "FIt" + + describeTable = "DescribeTable" + pdescribeTable = "PDescribeTable" + xdescribeTable = "XDescribeTable" + fdescribeTable = "FDescribeTable" + + entry = "Entry" + pentry = "PEntry" + xentry = "XEntry" + fentry = "FEntry" +) + +func isFocusContainer(name string) bool { + switch name { + case fdescribe, fcontext, fwhen, fit, fdescribeTable, fentry: + return true + } + return false +} + +func isContainer(name string) bool { + switch name { + case it, when, contextContainer, describe, describeTable, entry, + pit, pwhen, pcontext, pdescribe, pdescribeTable, pentry, + xit, xwhen, xcontext, xdescribe, xdescribeTable, xentry: + return true + } + return isFocusContainer(name) +} + +func isWrapContainer(name string) bool { + switch name { + case when, contextContainer, describe, + fwhen, fcontext, fdescribe, + pwhen, pcontext, pdescribe, + xwhen, xcontext, xdescribe: + return true + } + + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/handler.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/handler.go index f10d83184..c44e3e8d8 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/handler.go +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/handler.go @@ -2,6 +2,10 @@ package ginkgohandler import ( "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/types" ) const ( @@ -14,116 +18,31 @@ const ( // Handler provide different handling, depend on the way ginkgo was imported, whether // in imported with "." name, custom name or without any name. type Handler interface { - GetFocusContainerName(*ast.CallExpr) (bool, *ast.Ident) - IsWrapContainer(*ast.CallExpr) bool - IsFocusSpec(ident ast.Expr) bool + HandleGinkgoSpecs(ast.Expr, types.Config, *analysis.Pass) bool + getFocusContainerName(*ast.CallExpr) (bool, *ast.Ident) + isWrapContainer(*ast.CallExpr) bool + isFocusSpec(ident ast.Expr) bool } // GetGinkgoHandler returns a ginkgor handler according to the way ginkgo was imported in the specific file func GetGinkgoHandler(file *ast.File) Handler { for _, imp := range file.Imports { - if imp.Path.Value != importPath && imp.Path.Value != importPathV2 { - continue - } + switch imp.Path.Value { + + case importPath, importPathV2: + switch name := imp.Name.String(); { + case name == ".": + return dotHandler{} + case name == "": // import with no local name + return nameHandler("ginkgo") + default: + return nameHandler(name) + } - switch name := imp.Name.String(); { - case name == ".": - return dotHandler{} - case name == "": // import with no local name - return nameHandler("ginkgo") default: - return nameHandler(name) - } - } - - return nil // no ginkgo import; this file does not use ginkgo -} - -// dotHandler is used when importing ginkgo with dot; i.e. -// import . "github.com/onsi/ginkgo" -type dotHandler struct{} - -func (h dotHandler) GetFocusContainerName(exp *ast.CallExpr) (bool, *ast.Ident) { - if fun, ok := exp.Fun.(*ast.Ident); ok { - return isFocusContainer(fun.Name), fun - } - return false, nil -} - -func (h dotHandler) IsWrapContainer(exp *ast.CallExpr) bool { - if fun, ok := exp.Fun.(*ast.Ident); ok { - return IsWrapContainer(fun.Name) - } - return false -} - -func (h dotHandler) IsFocusSpec(exp ast.Expr) bool { - id, ok := exp.(*ast.Ident) - return ok && id.Name == focusSpec -} - -// nameHandler is used when importing ginkgo without name; i.e. -// import "github.com/onsi/ginkgo" -// -// or with a custom name; e.g. -// import customname "github.com/onsi/ginkgo" -type nameHandler string - -func (h nameHandler) GetFocusContainerName(exp *ast.CallExpr) (bool, *ast.Ident) { - if sel, ok := exp.Fun.(*ast.SelectorExpr); ok { - if id, ok := sel.X.(*ast.Ident); ok && id.Name == string(h) { - return isFocusContainer(sel.Sel.Name), sel.Sel - } - } - return false, nil -} - -func (h nameHandler) IsWrapContainer(exp *ast.CallExpr) bool { - if sel, ok := exp.Fun.(*ast.SelectorExpr); ok { - if id, ok := sel.X.(*ast.Ident); ok && id.Name == string(h) { - return IsWrapContainer(sel.Sel.Name) - } - } - return false - -} - -func (h nameHandler) IsFocusSpec(exp ast.Expr) bool { - if selExp, ok := exp.(*ast.SelectorExpr); ok { - if x, ok := selExp.X.(*ast.Ident); ok && x.Name == string(h) { - return selExp.Sel.Name == focusSpec + continue } } - return false -} - -func isFocusContainer(name string) bool { - switch name { - case "FDescribe", "FContext", "FWhen", "FIt", "FDescribeTable", "FEntry": - return true - } - return false -} - -func IsContainer(name string) bool { - switch name { - case "It", "When", "Context", "Describe", "DescribeTable", "Entry", - "PIt", "PWhen", "PContext", "PDescribe", "PDescribeTable", "PEntry", - "XIt", "XWhen", "XContext", "XDescribe", "XDescribeTable", "XEntry": - return true - } - return isFocusContainer(name) -} - -func IsWrapContainer(name string) bool { - switch name { - case "When", "Context", "Describe", - "FWhen", "FContext", "FDescribe", - "PWhen", "PContext", "PDescribe", - "XWhen", "XContext", "XDescribe": - return true - } - - return false + return nil } diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/handling.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/handling.go new file mode 100644 index 000000000..4b6de5767 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/handling.go @@ -0,0 +1,195 @@ +package ginkgohandler + +import ( + "fmt" + "go/ast" + "go/token" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/types" +) + +const ( + linterName = "ginkgo-linter" + focusContainerFound = linterName + ": Focus container found. This is used only for local debug and should not be part of the actual source code. Consider to replace with %q" + focusSpecFound = linterName + ": Focus spec found. This is used only for local debug and should not be part of the actual source code. Consider to remove it" + useBeforeEachTemplate = "use BeforeEach() to assign variable %s" +) + +func handleGinkgoSpecs(expr ast.Expr, config types.Config, pass *analysis.Pass, ginkgoHndlr Handler) bool { + goDeeper := false + if exp, ok := expr.(*ast.CallExpr); ok { + if bool(config.ForbidFocus) && checkFocusContainer(pass, ginkgoHndlr, exp) { + goDeeper = true + } + + if bool(config.ForbidSpecPollution) && checkAssignmentsInContainer(pass, ginkgoHndlr, exp) { + goDeeper = true + } + } + return goDeeper +} + +func checkAssignmentsInContainer(pass *analysis.Pass, ginkgoHndlr Handler, exp *ast.CallExpr) bool { + foundSomething := false + if ginkgoHndlr.isWrapContainer(exp) { + for _, arg := range exp.Args { + if fn, ok := arg.(*ast.FuncLit); ok { + if fn.Body != nil { + if checkAssignments(pass, fn.Body.List) { + foundSomething = true + } + break + } + } + } + } + + return foundSomething +} + +func checkAssignments(pass *analysis.Pass, list []ast.Stmt) bool { + foundSomething := false + for _, stmt := range list { + switch st := stmt.(type) { + case *ast.DeclStmt: + if checkAssignmentDecl(pass, st) { + foundSomething = true + } + + case *ast.AssignStmt: + if checkAssignmentAssign(pass, st) { + foundSomething = true + } + + case *ast.IfStmt: + if checkAssignmentIf(pass, st) { + foundSomething = true + } + } + } + + return foundSomething +} + +func checkAssignmentsValues(pass *analysis.Pass, names []*ast.Ident, values []ast.Expr) bool { + foundSomething := false + for i, val := range values { + if !is[*ast.FuncLit](val) { + reportNoFix(pass, names[i].Pos(), useBeforeEachTemplate, names[i].Name) + foundSomething = true + } + } + + return foundSomething +} + +func checkAssignmentDecl(pass *analysis.Pass, ds *ast.DeclStmt) bool { + foundSomething := false + if gen, ok := ds.Decl.(*ast.GenDecl); ok { + if gen.Tok != token.VAR { + return false + } + for _, spec := range gen.Specs { + if valSpec, ok := spec.(*ast.ValueSpec); ok { + if checkAssignmentsValues(pass, valSpec.Names, valSpec.Values) { + foundSomething = true + } + } + } + } + + return foundSomething +} + +func checkAssignmentAssign(pass *analysis.Pass, as *ast.AssignStmt) bool { + foundSomething := false + for i, val := range as.Rhs { + if !is[*ast.FuncLit](val) { + if id, isIdent := as.Lhs[i].(*ast.Ident); isIdent && id.Name != "_" { + reportNoFix(pass, id.Pos(), useBeforeEachTemplate, id.Name) + foundSomething = true + } + } + } + return foundSomething +} + +func checkAssignmentIf(pass *analysis.Pass, is *ast.IfStmt) bool { + foundSomething := false + + if is.Body != nil { + if checkAssignments(pass, is.Body.List) { + foundSomething = true + } + } + if is.Else != nil { + if block, isBlock := is.Else.(*ast.BlockStmt); isBlock { + if checkAssignments(pass, block.List) { + foundSomething = true + } + } + } + + return foundSomething +} + +func checkFocusContainer(pass *analysis.Pass, handler Handler, exp *ast.CallExpr) bool { + foundFocus := false + isFocus, id := handler.getFocusContainerName(exp) + if isFocus { + reportNewName(pass, id, id.Name[1:], id.Name) + foundFocus = true + } + + if id != nil && isContainer(id.Name) { + for _, arg := range exp.Args { + if handler.isFocusSpec(arg) { + reportNoFix(pass, arg.Pos(), focusSpecFound) + foundFocus = true + } else if callExp, ok := arg.(*ast.CallExpr); ok { + if checkFocusContainer(pass, handler, callExp) { // handle table entries + foundFocus = true + } + } + } + } + + return foundFocus +} + +func reportNewName(pass *analysis.Pass, id *ast.Ident, newName string, oldExpr string) { + pass.Report(analysis.Diagnostic{ + Pos: id.Pos(), + Message: fmt.Sprintf(focusContainerFound, newName), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("should replace %s with %s", oldExpr, newName), + TextEdits: []analysis.TextEdit{ + { + Pos: id.Pos(), + End: id.End(), + NewText: []byte(newName), + }, + }, + }, + }, + }) +} + +func reportNoFix(pass *analysis.Pass, pos token.Pos, message string, args ...any) { + if len(args) > 0 { + message = fmt.Sprintf(message, args...) + } + + pass.Report(analysis.Diagnostic{ + Pos: pos, + Message: message, + }) +} + +func is[T any](x any) bool { + _, matchType := x.(T) + return matchType +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/namehandler.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/namehandler.go new file mode 100644 index 000000000..2ef9fe703 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/ginkgohandler/namehandler.go @@ -0,0 +1,49 @@ +package ginkgohandler + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/types" +) + +// nameHandler is used when importing ginkgo without name; i.e. +// import "github.com/onsi/ginkgo" +// +// or with a custom name; e.g. +// import customname "github.com/onsi/ginkgo" +type nameHandler string + +func (h nameHandler) HandleGinkgoSpecs(expr ast.Expr, config types.Config, pass *analysis.Pass) bool { + return handleGinkgoSpecs(expr, config, pass, h) +} + +func (h nameHandler) getFocusContainerName(exp *ast.CallExpr) (bool, *ast.Ident) { + if sel, ok := exp.Fun.(*ast.SelectorExpr); ok { + if id, ok := sel.X.(*ast.Ident); ok && id.Name == string(h) { + return isFocusContainer(sel.Sel.Name), sel.Sel + } + } + return false, nil +} + +func (h nameHandler) isWrapContainer(exp *ast.CallExpr) bool { + if sel, ok := exp.Fun.(*ast.SelectorExpr); ok { + if id, ok := sel.X.(*ast.Ident); ok && id.Name == string(h) { + return isWrapContainer(sel.Sel.Name) + } + } + return false + +} + +func (h nameHandler) isFocusSpec(exp ast.Expr) bool { + if selExp, ok := exp.(*ast.SelectorExpr); ok { + if x, ok := selExp.X.(*ast.Ident); ok && x.Name == string(h) { + return selExp.Sel.Name == focusSpec + } + } + + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/dothandler.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/dothandler.go new file mode 100644 index 000000000..bd3b93992 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/dothandler.go @@ -0,0 +1,99 @@ +package gomegahandler + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/gomegainfo" +) + +// dotHandler is used when importing gomega with dot; i.e. +// import . "github.com/onsi/gomega" +type dotHandler struct { + pass *analysis.Pass +} + +// GetActualFuncName returns the name of the gomega function, e.g. `Expect` +func (h dotHandler) GetActualFuncName(expr *ast.CallExpr) (string, bool) { + switch actualFunc := expr.Fun.(type) { + case *ast.Ident: + return actualFunc.Name, true + case *ast.SelectorExpr: + if h.isGomegaVar(actualFunc.X) { + return actualFunc.Sel.Name, true + } + + if x, ok := actualFunc.X.(*ast.CallExpr); ok { + return h.GetActualFuncName(x) + } + + case *ast.CallExpr: + return h.GetActualFuncName(actualFunc) + } + return "", false +} + +// ReplaceFunction replaces the function with another one, for fix suggestions +func (dotHandler) ReplaceFunction(caller *ast.CallExpr, newExpr *ast.Ident) { + switch f := caller.Fun.(type) { + case *ast.Ident: + caller.Fun = newExpr + case *ast.SelectorExpr: + f.Sel = newExpr + } +} + +func (dotHandler) GetNewWrapperMatcher(name string, existing *ast.CallExpr) *ast.CallExpr { + return &ast.CallExpr{ + Fun: ast.NewIdent(name), + Args: []ast.Expr{existing}, + } +} + +func (h dotHandler) GetActualExpr(assertionFunc *ast.SelectorExpr) *ast.CallExpr { + actualExpr, ok := assertionFunc.X.(*ast.CallExpr) + if !ok { + return nil + } + + switch fun := actualExpr.Fun.(type) { + case *ast.Ident: + return actualExpr + case *ast.SelectorExpr: + if gomegainfo.IsActualMethod(fun.Sel.Name) { + if h.isGomegaVar(fun.X) { + return actualExpr + } + } else { + return h.GetActualExpr(fun) + } + } + return nil +} + +func (h dotHandler) GetActualExprClone(origFunc, funcClone *ast.SelectorExpr) *ast.CallExpr { + actualExpr, ok := funcClone.X.(*ast.CallExpr) + if !ok { + return nil + } + + switch funClone := actualExpr.Fun.(type) { + case *ast.Ident: + return actualExpr + case *ast.SelectorExpr: + origFun := origFunc.X.(*ast.CallExpr).Fun.(*ast.SelectorExpr) + if gomegainfo.IsActualMethod(funClone.Sel.Name) { + if h.isGomegaVar(origFun.X) { + return actualExpr + } + } else { + return h.GetActualExprClone(origFun, funClone) + } + } + return nil +} + +func (h dotHandler) isGomegaVar(x ast.Expr) bool { + return gomegainfo.IsGomegaVar(x, h.pass) +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/handler.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/handler.go index 4290e7373..4dba604a4 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/handler.go +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/handler.go @@ -2,7 +2,8 @@ package gomegahandler import ( "go/ast" - "go/token" + + "golang.org/x/tools/go/analysis" ) const ( @@ -17,15 +18,15 @@ type Handler interface { // ReplaceFunction replaces the function with another one, for fix suggestions ReplaceFunction(*ast.CallExpr, *ast.Ident) - getDefFuncName(expr *ast.CallExpr) string + GetActualExpr(assertionFunc *ast.SelectorExpr) *ast.CallExpr - getFieldType(field *ast.Field) string + GetActualExprClone(origFunc, funcClone *ast.SelectorExpr) *ast.CallExpr - GetActualExpr(assertionFunc *ast.SelectorExpr) *ast.CallExpr + GetNewWrapperMatcher(name string, existing *ast.CallExpr) *ast.CallExpr } // GetGomegaHandler returns a gomegar handler according to the way gomega was imported in the specific file -func GetGomegaHandler(file *ast.File) Handler { +func GetGomegaHandler(file *ast.File, pass *analysis.Pass) Handler { for _, imp := range file.Imports { if imp.Path.Value != importPath { continue @@ -33,209 +34,15 @@ func GetGomegaHandler(file *ast.File) Handler { switch name := imp.Name.String(); { case name == ".": - return dotHandler{} + return &dotHandler{ + pass: pass, + } case name == "": // import with no local name - return nameHandler("gomega") + return &nameHandler{name: "gomega", pass: pass} default: - return nameHandler(name) + return &nameHandler{name: name, pass: pass} } } return nil // no gomega import; this file does not use gomega } - -// dotHandler is used when importing gomega with dot; i.e. -// import . "github.com/onsi/gomega" -type dotHandler struct{} - -// GetActualFuncName returns the name of the gomega function, e.g. `Expect` -func (h dotHandler) GetActualFuncName(expr *ast.CallExpr) (string, bool) { - switch actualFunc := expr.Fun.(type) { - case *ast.Ident: - return actualFunc.Name, true - case *ast.SelectorExpr: - if isGomegaVar(actualFunc.X, h) { - return actualFunc.Sel.Name, true - } - - if x, ok := actualFunc.X.(*ast.CallExpr); ok { - return h.GetActualFuncName(x) - } - - case *ast.CallExpr: - return h.GetActualFuncName(actualFunc) - } - return "", false -} - -// ReplaceFunction replaces the function with another one, for fix suggestions -func (dotHandler) ReplaceFunction(caller *ast.CallExpr, newExpr *ast.Ident) { - switch f := caller.Fun.(type) { - case *ast.Ident: - caller.Fun = newExpr - case *ast.SelectorExpr: - f.Sel = newExpr - } -} - -func (dotHandler) getDefFuncName(expr *ast.CallExpr) string { - if f, ok := expr.Fun.(*ast.Ident); ok { - return f.Name - } - return "" -} - -func (dotHandler) getFieldType(field *ast.Field) string { - switch t := field.Type.(type) { - case *ast.Ident: - return t.Name - case *ast.StarExpr: - if name, ok := t.X.(*ast.Ident); ok { - return name.Name - } - } - return "" -} - -// nameHandler is used when importing gomega without name; i.e. -// import "github.com/onsi/gomega" -// -// or with a custom name; e.g. -// import customname "github.com/onsi/gomega" -type nameHandler string - -// GetActualFuncName returns the name of the gomega function, e.g. `Expect` -func (g nameHandler) GetActualFuncName(expr *ast.CallExpr) (string, bool) { - selector, ok := expr.Fun.(*ast.SelectorExpr) - if !ok { - return "", false - } - - switch x := selector.X.(type) { - case *ast.Ident: - if x.Name != string(g) { - if !isGomegaVar(x, g) { - return "", false - } - } - - return selector.Sel.Name, true - - case *ast.CallExpr: - return g.GetActualFuncName(x) - } - - return "", false -} - -// ReplaceFunction replaces the function with another one, for fix suggestions -func (nameHandler) ReplaceFunction(caller *ast.CallExpr, newExpr *ast.Ident) { - caller.Fun.(*ast.SelectorExpr).Sel = newExpr -} - -func (g nameHandler) getDefFuncName(expr *ast.CallExpr) string { - if sel, ok := expr.Fun.(*ast.SelectorExpr); ok { - if f, ok := sel.X.(*ast.Ident); ok && f.Name == string(g) { - return sel.Sel.Name - } - } - return "" -} - -func (g nameHandler) getFieldType(field *ast.Field) string { - switch t := field.Type.(type) { - case *ast.SelectorExpr: - if id, ok := t.X.(*ast.Ident); ok { - if id.Name == string(g) { - return t.Sel.Name - } - } - case *ast.StarExpr: - if sel, ok := t.X.(*ast.SelectorExpr); ok { - if x, ok := sel.X.(*ast.Ident); ok && x.Name == string(g) { - return sel.Sel.Name - } - } - - } - return "" -} - -func isGomegaVar(x ast.Expr, handler Handler) bool { - if i, ok := x.(*ast.Ident); ok { - if i.Obj != nil && i.Obj.Kind == ast.Var { - switch decl := i.Obj.Decl.(type) { - case *ast.AssignStmt: - if decl.Tok == token.DEFINE { - if defFunc, ok := decl.Rhs[0].(*ast.CallExpr); ok { - fName := handler.getDefFuncName(defFunc) - switch fName { - case "NewGomega", "NewWithT", "NewGomegaWithT": - return true - } - } - } - case *ast.Field: - name := handler.getFieldType(decl) - switch name { - case "Gomega", "WithT", "GomegaWithT": - return true - } - } - } - } - return false -} - -func (h dotHandler) GetActualExpr(assertionFunc *ast.SelectorExpr) *ast.CallExpr { - actualExpr, ok := assertionFunc.X.(*ast.CallExpr) - if !ok { - return nil - } - - switch fun := actualExpr.Fun.(type) { - case *ast.Ident: - return actualExpr - case *ast.SelectorExpr: - if isHelperMethods(fun.Sel.Name) { - return h.GetActualExpr(fun) - } - if isGomegaVar(fun.X, h) { - return actualExpr - } - } - return nil -} - -func (g nameHandler) GetActualExpr(assertionFunc *ast.SelectorExpr) *ast.CallExpr { - actualExpr, ok := assertionFunc.X.(*ast.CallExpr) - if !ok { - return nil - } - - switch fun := actualExpr.Fun.(type) { - case *ast.Ident: - return actualExpr - case *ast.SelectorExpr: - if x, ok := fun.X.(*ast.Ident); ok && x.Name == string(g) { - return actualExpr - } - if isHelperMethods(fun.Sel.Name) { - return g.GetActualExpr(fun) - } - - if isGomegaVar(fun.X, g) { - return actualExpr - } - } - return nil -} - -func isHelperMethods(funcName string) bool { - switch funcName { - case "WithOffset", "WithTimeout", "WithPolling", "Within", "ProbeEvery", "WithContext", "WithArguments", "MustPassRepeatedly": - return true - } - - return false -} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/namedhandler.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/namedhandler.go new file mode 100644 index 000000000..712442426 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegahandler/namedhandler.go @@ -0,0 +1,112 @@ +package gomegahandler + +import ( + "go/ast" + + "github.com/nunnatsa/ginkgolinter/internal/gomegainfo" + + "golang.org/x/tools/go/analysis" +) + +// nameHandler is used when importing gomega without name; i.e. +// import "github.com/onsi/gomega" +// +// or with a custom name; e.g. +// import customname "github.com/onsi/gomega" +type nameHandler struct { + name string + pass *analysis.Pass +} + +// GetActualFuncName returns the name of the gomega function, e.g. `Expect` +func (g nameHandler) GetActualFuncName(expr *ast.CallExpr) (string, bool) { + selector, ok := expr.Fun.(*ast.SelectorExpr) + if !ok { + return "", false + } + + switch x := selector.X.(type) { + case *ast.Ident: + if x.Name != g.name { + if !g.isGomegaVar(x) { + return "", false + } + } + + return selector.Sel.Name, true + + case *ast.CallExpr: + return g.GetActualFuncName(x) + } + + return "", false +} + +// ReplaceFunction replaces the function with another one, for fix suggestions +func (nameHandler) ReplaceFunction(caller *ast.CallExpr, newExpr *ast.Ident) { + caller.Fun.(*ast.SelectorExpr).Sel = newExpr +} + +func (g nameHandler) isGomegaVar(x ast.Expr) bool { + return gomegainfo.IsGomegaVar(x, g.pass) +} + +func (g nameHandler) GetActualExpr(assertionFunc *ast.SelectorExpr) *ast.CallExpr { + actualExpr, ok := assertionFunc.X.(*ast.CallExpr) + if !ok { + return nil + } + + switch fun := actualExpr.Fun.(type) { + case *ast.Ident: + return actualExpr + case *ast.SelectorExpr: + if x, ok := fun.X.(*ast.Ident); ok && x.Name == g.name { + return actualExpr + } + if gomegainfo.IsActualMethod(fun.Sel.Name) { + if g.isGomegaVar(fun.X) { + return actualExpr + } + } else { + return g.GetActualExpr(fun) + } + } + return nil +} + +func (g nameHandler) GetActualExprClone(origFunc, funcClone *ast.SelectorExpr) *ast.CallExpr { + actualExpr, ok := funcClone.X.(*ast.CallExpr) + if !ok { + return nil + } + + switch funClone := actualExpr.Fun.(type) { + case *ast.Ident: + return actualExpr + case *ast.SelectorExpr: + if x, ok := funClone.X.(*ast.Ident); ok && x.Name == g.name { + return actualExpr + } + origFun := origFunc.X.(*ast.CallExpr).Fun.(*ast.SelectorExpr) + if gomegainfo.IsActualMethod(funClone.Sel.Name) { + if g.isGomegaVar(origFun.X) { + return actualExpr + } + } else { + return g.GetActualExprClone(origFun, funClone) + } + + } + return nil +} + +func (g nameHandler) GetNewWrapperMatcher(name string, existing *ast.CallExpr) *ast.CallExpr { + return &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: ast.NewIdent(g.name), + Sel: ast.NewIdent(name), + }, + Args: []ast.Expr{existing}, + } +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegainfo/gomegainfo.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegainfo/gomegainfo.go new file mode 100644 index 000000000..ca45a34b2 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/gomegainfo/gomegainfo.go @@ -0,0 +1,113 @@ +package gomegainfo + +import ( + "go/ast" + gotypes "go/types" + "regexp" + + "golang.org/x/tools/go/analysis" +) + +const ( // gomega actual method names + expect = "Expect" + expectWithOffset = "ExpectWithOffset" + omega = "Ω" + eventually = "Eventually" + eventuallyWithOffset = "EventuallyWithOffset" + consistently = "Consistently" + consistentlyWithOffset = "ConsistentlyWithOffset" +) + +const ( // assertion methods + to = "To" + toNot = "ToNot" + notTo = "NotTo" + should = "Should" + shouldNot = "ShouldNot" +) + +var funcOffsetMap = map[string]int{ + expect: 0, + expectWithOffset: 1, + omega: 0, + eventually: 0, + eventuallyWithOffset: 1, + consistently: 0, + consistentlyWithOffset: 1, +} + +func IsActualMethod(name string) bool { + _, found := funcOffsetMap[name] + return found +} + +func ActualArgOffset(methodName string) int { + funcOffset, ok := funcOffsetMap[methodName] + if !ok { + return -1 + } + return funcOffset +} + +func GetAllowedAssertionMethods(actualMethodName string) string { + switch actualMethodName { + case expect, expectWithOffset: + return `"To()", "ToNot()" or "NotTo()"` + + case eventually, eventuallyWithOffset, consistently, consistentlyWithOffset: + return `"Should()" or "ShouldNot()"` + + case omega: + return `"Should()", "To()", "ShouldNot()", "ToNot()" or "NotTo()"` + + default: + return "" + } +} + +var asyncFuncSet = map[string]struct{}{ + eventually: {}, + eventuallyWithOffset: {}, + consistently: {}, + consistentlyWithOffset: {}, +} + +func IsAsyncActualMethod(name string) bool { + _, ok := asyncFuncSet[name] + return ok +} + +func IsAssertionFunc(name string) bool { + switch name { + case to, toNot, notTo, should, shouldNot: + return true + } + return false +} + +var gomegaTypeRegex = regexp.MustCompile(`github\.com/onsi/gomega/(?:internal|types)\.Gomega`) + +func IsGomegaVar(x ast.Expr, pass *analysis.Pass) bool { + if tx, ok := pass.TypesInfo.Types[x]; ok { + return IsGomegaType(tx.Type) + } + + return false +} + +func IsGomegaType(t gotypes.Type) bool { + var typeStr string + switch ttx := t.(type) { + case *gotypes.Pointer: + tp := ttx.Elem() + typeStr = tp.String() + + case *gotypes.Named: + typeStr = ttx.String() + + default: + return false + } + + return gomegaTypeRegex.MatchString(typeStr) +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/interfaces/interfaces.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/interfaces/interfaces.go index dafeacd4f..91849ca56 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/internal/interfaces/interfaces.go +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/interfaces/interfaces.go @@ -72,5 +72,5 @@ func ImplementsError(t gotypes.Type) bool { } func ImplementsGomegaMatcher(t gotypes.Type) bool { - return gotypes.Implements(t, gomegaMatcherType) + return t != nil && gotypes.Implements(t, gomegaMatcherType) } diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/intervals/intervals.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/intervals/intervals.go index b8166bdb2..51d55166d 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/internal/intervals/intervals.go +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/intervals/intervals.go @@ -1,285 +1,166 @@ package intervals import ( - "errors" "go/ast" "go/constant" "go/token" gotypes "go/types" - "strconv" "time" "golang.org/x/tools/go/analysis" - - "github.com/nunnatsa/ginkgolinter/internal/gomegahandler" - "github.com/nunnatsa/ginkgolinter/internal/reports" ) -type noDurationIntervalErr struct { - value string -} - -func (err noDurationIntervalErr) Error() string { - return "only use time.Duration for timeout and polling in Eventually() or Consistently()" -} - -func CheckIntervals(pass *analysis.Pass, expr *ast.CallExpr, actualExpr *ast.CallExpr, reportBuilder *reports.Builder, handler gomegahandler.Handler, timePkg string, funcIndex int) { - var ( - timeout time.Duration - polling time.Duration - err error - ) - - timeoutOffset := funcIndex + 1 - if len(actualExpr.Args) > timeoutOffset { - timeout, err = getDuration(pass, actualExpr.Args[timeoutOffset], timePkg) - if err != nil { - suggestFix := false - if tryFixIntDuration(expr, err, handler, timePkg, timeoutOffset) { - suggestFix = true - } - reportBuilder.AddIssue(suggestFix, err.Error()) - } - pollingOffset := funcIndex + 2 - if len(actualExpr.Args) > pollingOffset { - polling, err = getDuration(pass, actualExpr.Args[pollingOffset], timePkg) - if err != nil { - suggestFix := false - if tryFixIntDuration(expr, err, handler, timePkg, pollingOffset) { - suggestFix = true +func GetDuration(pass *analysis.Pass, argOffset int, origInterval, intervalClone ast.Expr, timePkg string) DurationValue { + tv := pass.TypesInfo.Types[origInterval] + argType := tv.Type + if durType, ok := argType.(*gotypes.Named); ok { + if durType.String() == "time.Duration" { + if tv.Value != nil { + if val, ok := constant.Int64Val(tv.Value); ok { + return &RealDurationValue{ + dur: time.Duration(val), + expr: intervalClone, + } } - reportBuilder.AddIssue(suggestFix, err.Error()) + } + return &UnknownDurationTypeValue{ + expr: intervalClone, } } } - selExp := expr.Fun.(*ast.SelectorExpr) - for { - call, ok := selExp.X.(*ast.CallExpr) - if !ok { - break - } - - fun, ok := call.Fun.(*ast.SelectorExpr) - if !ok { - break - } - - switch fun.Sel.Name { - case "WithTimeout", "Within": - if timeout != 0 { - reportBuilder.AddIssue(false, "timeout defined more than once") - } else if len(call.Args) == 1 { - timeout, err = getDurationFromValue(pass, call.Args[0], timePkg) - if err != nil { - reportBuilder.AddIssue(false, err.Error()) + if basic, ok := argType.(*gotypes.Basic); ok && tv.Value != nil { + if basic.Info()&gotypes.IsInteger != 0 { + if num, ok := constant.Int64Val(tv.Value); ok { + return &NumericDurationValue{ + timePkg: timePkg, + numSeconds: num, + offset: argOffset, + dur: time.Duration(num) * time.Second, + expr: intervalClone, } } + } - case "WithPolling", "ProbeEvery": - if polling != 0 { - reportBuilder.AddIssue(false, "polling defined more than once") - } else if len(call.Args) == 1 { - polling, err = getDurationFromValue(pass, call.Args[0], timePkg) - if err != nil { - reportBuilder.AddIssue(false, err.Error()) + if basic.Info()&gotypes.IsFloat != 0 { + if num, ok := constant.Float64Val(tv.Value); ok { + return &NumericDurationValue{ + timePkg: timePkg, + numSeconds: int64(num), + offset: argOffset, + dur: time.Duration(num) * time.Second, + expr: intervalClone, } } } - - selExp = fun } - if timeout != 0 && polling != 0 && timeout < polling { - reportBuilder.AddIssue(false, "timeout must not be shorter than the polling interval") - } + return &UnknownDurationValue{expr: intervalClone} } -func tryFixIntDuration(expr *ast.CallExpr, err error, handler gomegahandler.Handler, timePkg string, offset int) bool { - suggestFix := false - var durErr noDurationIntervalErr - if errors.As(err, &durErr) { - if len(durErr.value) > 0 { - actualExpr := handler.GetActualExpr(expr.Fun.(*ast.SelectorExpr)) - var newArg ast.Expr - second := &ast.SelectorExpr{ - Sel: ast.NewIdent("Second"), - X: ast.NewIdent(timePkg), +func GetDurationFromValue(pass *analysis.Pass, orig, clone ast.Expr) DurationValue { + tv := pass.TypesInfo.Types[orig] + interval := tv.Value + if interval != nil { + if val, ok := constant.Int64Val(interval); ok { + return RealDurationValue{ + dur: time.Duration(val), + expr: orig, } - if durErr.value == "1" { - newArg = second - } else { - newArg = &ast.BinaryExpr{ - X: second, - Op: token.MUL, - Y: actualExpr.Args[offset], - } - } - actualExpr.Args[offset] = newArg - suggestFix = true } } - - return suggestFix + return UnknownDurationTypeValue{expr: clone} } -func getDuration(pass *analysis.Pass, interval ast.Expr, timePkg string) (time.Duration, error) { - argType := pass.TypesInfo.TypeOf(interval) - if durType, ok := argType.(*gotypes.Named); ok { - if durType.Obj().Name() == "Duration" && durType.Obj().Pkg().Name() == "time" { - return getDurationFromValue(pass, interval, timePkg) - } - } +type DurationValue interface { + Duration() time.Duration +} - value := "" - switch val := interval.(type) { - case *ast.BasicLit: - if val.Kind == token.INT { - value = val.Value - } - case *ast.Ident: - i, err := getConstDuration(pass, val, timePkg) - if err != nil || i == 0 { - return 0, nil - } - value = val.Name - } +type NumericValue interface { + GetOffset() int + GetDurationExpr() ast.Expr +} +type RealDurationValue struct { + dur time.Duration + expr ast.Expr +} - return 0, noDurationIntervalErr{value: value} +func (r RealDurationValue) Duration() time.Duration { + return r.dur } -func getDurationFromValue(pass *analysis.Pass, interval ast.Expr, timePkg string) (time.Duration, error) { - switch dur := interval.(type) { - case *ast.SelectorExpr: - ident, ok := dur.X.(*ast.Ident) - if ok { - if ident.Name == timePkg { - return getTimeDurationValue(dur) - } - return getDurationFromValue(pass, dur.Sel, timePkg) - } - case *ast.BinaryExpr: - return getBinaryExprDuration(pass, dur, timePkg) +type NumericDurationValue struct { + timePkg string + numSeconds int64 + offset int + dur time.Duration + expr ast.Expr +} - case *ast.Ident: - return getConstDuration(pass, dur, timePkg) - } +func (r *NumericDurationValue) Duration() time.Duration { + return r.dur +} - return 0, nil +func (r *NumericDurationValue) GetOffset() int { + return r.offset } -func getConstDuration(pass *analysis.Pass, ident *ast.Ident, timePkg string) (time.Duration, error) { - o := pass.TypesInfo.ObjectOf(ident) - if o != nil { - if c, ok := o.(*gotypes.Const); ok { - if c.Val().Kind() == constant.Int { - i, err := strconv.Atoi(c.Val().String()) - if err != nil { - return 0, nil - } - return time.Duration(i), nil - } - } +func (r *NumericDurationValue) GetDurationExpr() ast.Expr { + var newArg ast.Expr + second := &ast.SelectorExpr{ + Sel: ast.NewIdent("Second"), + X: ast.NewIdent(r.timePkg), } - if ident.Obj != nil && ident.Obj.Kind == ast.Con && ident.Obj.Decl != nil { - if vals, ok := ident.Obj.Decl.(*ast.ValueSpec); ok { - if len(vals.Values) == 1 { - switch val := vals.Values[0].(type) { - case *ast.BasicLit: - if val.Kind == token.INT { - i, err := strconv.Atoi(val.Value) - if err != nil { - return 0, nil - } - return time.Duration(i), nil - } - return 0, nil - case *ast.BinaryExpr: - return getBinaryExprDuration(pass, val, timePkg) - } - } + if r.numSeconds == 1 { + newArg = second + } else { + newArg = &ast.BinaryExpr{ + X: second, + Op: token.MUL, + Y: r.expr, } } - return 0, nil + return newArg } -func getTimeDurationValue(dur *ast.SelectorExpr) (time.Duration, error) { - switch dur.Sel.Name { - case "Nanosecond": - return time.Nanosecond, nil - case "Microsecond": - return time.Microsecond, nil - case "Millisecond": - return time.Millisecond, nil - case "Second": - return time.Second, nil - case "Minute": - return time.Minute, nil - case "Hour": - return time.Hour, nil - default: - return 0, errors.New("unknown duration value") // should never happen - } +type UnknownDurationValue struct { + expr ast.Expr } -func getBinaryExprDuration(pass *analysis.Pass, expr *ast.BinaryExpr, timePkg string) (time.Duration, error) { - x, err := getBinaryDurValue(pass, expr.X, timePkg) - if err != nil || x == 0 { - return 0, nil - } - y, err := getBinaryDurValue(pass, expr.Y, timePkg) - if err != nil || y == 0 { - return 0, nil - } +func (r UnknownDurationValue) Duration() time.Duration { + return 0 +} - switch expr.Op { - case token.ADD: - return x + y, nil - case token.SUB: - val := x - y - if val > 0 { - return val, nil - } - return 0, nil - case token.MUL: - return x * y, nil - case token.QUO: - if y == 0 { - return 0, nil - } - return x / y, nil - case token.REM: - if y == 0 { - return 0, nil - } - return x % y, nil - default: - return 0, nil - } +type UnknownNumericValue struct { + expr ast.Expr + offset int } -func getBinaryDurValue(pass *analysis.Pass, expr ast.Expr, timePkg string) (time.Duration, error) { - switch x := expr.(type) { - case *ast.SelectorExpr: - return getDurationFromValue(pass, x, timePkg) - case *ast.BinaryExpr: - return getBinaryExprDuration(pass, x, timePkg) - case *ast.BasicLit: - if x.Kind == token.INT { - val, err := strconv.Atoi(x.Value) - if err != nil { - return 0, err - } - return time.Duration(val), nil - } - case *ast.ParenExpr: - return getBinaryDurValue(pass, x.X, timePkg) +func (r UnknownNumericValue) Duration() time.Duration { + return 0 +} - case *ast.Ident: - return getConstDuration(pass, x, timePkg) +func (r UnknownNumericValue) GetDurationExpr() ast.Expr { + return &ast.BinaryExpr{ + X: &ast.SelectorExpr{ + Sel: ast.NewIdent("Second"), + X: ast.NewIdent("time"), + }, + Op: token.MUL, + Y: r.expr, } +} + +func (r UnknownNumericValue) GetOffset() int { + return r.offset +} + +type UnknownDurationTypeValue struct { + expr ast.Expr +} - return 0, nil +func (r UnknownDurationTypeValue) Duration() time.Duration { + return 0 } diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/reports/report-builder.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/reports/report-builder.go index c7f931ca7..dee88bd2c 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/internal/reports/report-builder.go +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/reports/report-builder.go @@ -1,13 +1,13 @@ package reports import ( - "bytes" "fmt" "go/ast" - "go/printer" "go/token" "strings" + "github.com/nunnatsa/ginkgolinter/internal/formatter" + "golang.org/x/tools/go/analysis" ) @@ -18,19 +18,25 @@ type Builder struct { issues []string fixOffer string suggestFix bool + formatter *formatter.GoFmtFormatter } -func NewBuilder(fset *token.FileSet, oldExpr ast.Expr) *Builder { +func NewBuilder(oldExpr ast.Expr, expFormatter *formatter.GoFmtFormatter) *Builder { b := &Builder{ pos: oldExpr.Pos(), end: oldExpr.End(), - oldExpr: goFmt(fset, oldExpr), + oldExpr: expFormatter.Format(oldExpr), suggestFix: false, + formatter: expFormatter, } return b } +func (b *Builder) OldExp() string { + return b.oldExpr +} + func (b *Builder) AddIssue(suggestFix bool, issue string, args ...any) { if len(args) > 0 { issue = fmt.Sprintf(issue, args...) @@ -42,9 +48,11 @@ func (b *Builder) AddIssue(suggestFix bool, issue string, args ...any) { } } -func (b *Builder) SetFixOffer(fset *token.FileSet, fixOffer ast.Expr) { - if offer := goFmt(fset, fixOffer); offer != b.oldExpr { - b.fixOffer = offer +func (b *Builder) SetFixOffer(fixOffer ast.Expr) { + if b.suggestFix { + if offer := b.formatter.Format(fixOffer); offer != b.oldExpr { + b.fixOffer = offer + } } } @@ -76,10 +84,8 @@ func (b *Builder) Build() analysis.Diagnostic { return diagnostic } -func goFmt(fset *token.FileSet, x ast.Expr) string { - var b bytes.Buffer - _ = printer.Fprint(&b, fset, x) - return b.String() +func (b *Builder) FormatExpr(expr ast.Expr) string { + return b.formatter.Format(expr) } func (b *Builder) getMessage() string { diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asyncfunccallrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asyncfunccallrule.go new file mode 100644 index 000000000..e4eda7f6c --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asyncfunccallrule.go @@ -0,0 +1,41 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const valueInEventually = "use a function call in %[1]s. This actually checks nothing, because %[1]s receives the function returned value, instead of function itself, and this value is never changed" + +// AsyncFuncCallRule checks that there is no function call actual parameter, +// in an async actual method (e.g. Eventually). +// +// Async actual methods should get the function itself, not a function call, because +// then there is no async operation at all, and we're waiting for the function to be +// returned before calling the assertion. +// +// We do allow functions that return a function, a channel or a pointer. +type AsyncFuncCallRule struct{} + +func (r AsyncFuncCallRule) isApplied(gexp *expression.GomegaExpression, config types.Config) bool { + if bool(config.SuppressAsync) || !gexp.IsAsync() { + return false + } + + if asyncArg := gexp.GetAsyncActualArg(); asyncRules != nil { + return !asyncArg.IsValid() + } + + return false +} + +func (r AsyncFuncCallRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if r.isApplied(gexp, config) { + + gexp.AppendWithArgsToActual() + + reportBuilder.AddIssue(true, valueInEventually, gexp.GetActualFuncName()) + } + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asyncsucceedrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asyncsucceedrule.go new file mode 100644 index 000000000..803c705de --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asyncsucceedrule.go @@ -0,0 +1,30 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +type AsyncSucceedRule struct{} + +func (AsyncSucceedRule) isApply(gexp *expression.GomegaExpression) bool { + return gexp.IsAsync() && + gexp.MatcherTypeIs(matcher.SucceedMatcherType) && + gexp.ActualArgTypeIs(actual.FuncSigArgType) && + !gexp.ActualArgTypeIs(actual.ErrorTypeArgType|actual.GomegaParamArgType) +} + +func (r AsyncSucceedRule) Apply(gexp *expression.GomegaExpression, _ types.Config, reportBuilder *reports.Builder) bool { + if r.isApply(gexp) { + if gexp.ActualArgTypeIs(actual.MultiRetsArgType) { + reportBuilder.AddIssue(false, "Success matcher does not support multiple values") + } else { + reportBuilder.AddIssue(false, "Success matcher only support a single error value, or function with Gomega as its first parameter") + } + } + + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asynctimeintervalsrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asynctimeintervalsrule.go new file mode 100644 index 000000000..45953ec01 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/asynctimeintervalsrule.go @@ -0,0 +1,79 @@ +package rules + +import ( + "go/ast" + "time" + + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/intervals" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const ( + multipleTimeouts = "timeout defined more than once" + multiplePolling = "polling defined more than once" + onlyUseTimeDurationForInterval = "only use time.Duration for timeout and polling in Eventually() or Consistently()" + pollingGreaterThanTimeout = "timeout must not be shorter than the polling interval" +) + +type AsyncTimeIntervalsRule struct{} + +func (r AsyncTimeIntervalsRule) isApplied(gexp *expression.GomegaExpression, config types.Config) bool { + return !bool(config.SuppressAsync) && bool(config.ValidateAsyncIntervals) && gexp.IsAsync() +} + +func (r AsyncTimeIntervalsRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if r.isApplied(gexp, config) { + asyncArg := gexp.GetAsyncActualArg() + if asyncArg.TooManyTimeouts() { + reportBuilder.AddIssue(false, multipleTimeouts) + } + + if asyncArg.TooManyPolling() { + reportBuilder.AddIssue(false, multiplePolling) + } + + timeoutDuration := checkInterval(gexp, asyncArg.Timeout(), reportBuilder) + pollingDuration := checkInterval(gexp, asyncArg.Polling(), reportBuilder) + + if timeoutDuration > 0 && pollingDuration > 0 && pollingDuration > timeoutDuration { + reportBuilder.AddIssue(false, pollingGreaterThanTimeout) + } + } + + return false +} + +func checkInterval(gexp *expression.GomegaExpression, durVal intervals.DurationValue, reportBuilder *reports.Builder) time.Duration { + if durVal != nil { + switch to := durVal.(type) { + case *intervals.RealDurationValue, *intervals.UnknownDurationTypeValue: + + case *intervals.NumericDurationValue: + if checkNumericInterval(gexp.GetActualClone(), to) { + reportBuilder.AddIssue(true, onlyUseTimeDurationForInterval) + } + + case *intervals.UnknownDurationValue: + reportBuilder.AddIssue(true, onlyUseTimeDurationForInterval) + } + + return durVal.Duration() + } + + return 0 +} + +func checkNumericInterval(intervalMethod *ast.CallExpr, interval intervals.DurationValue) bool { + if interval != nil { + if numVal, ok := interval.(intervals.NumericValue); ok { + if offset := numVal.GetOffset(); offset > 0 { + intervalMethod.Args[offset] = numVal.GetDurationExpr() + return true + } + } + } + + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/caprule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/caprule.go new file mode 100644 index 000000000..e3ad45d96 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/caprule.go @@ -0,0 +1,128 @@ +package rules + +import ( + "go/token" + + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const wrongCapWarningTemplate = "wrong cap assertion" + +// CapRule does not allow using the cap() function in actual with numeric comparison. +// it suggests to use the HaveLen matcher, instead. +type CapRule struct{} + +func (r *CapRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + + if !r.isApplied(gexp, config) { + return false + } + + if r.fixExpression(gexp) { + reportBuilder.AddIssue(true, wrongCapWarningTemplate) + return true + } + return false +} + +func (r *CapRule) isApplied(gexp *expression.GomegaExpression, config types.Config) bool { + if config.SuppressLen { + return false + } + + //matcherType := gexp.matcher.GetMatcherInfo().Type() + if gexp.ActualArgTypeIs(actual.CapFuncActualArgType) { + if gexp.MatcherTypeIs(matcher.EqualMatcherType | matcher.BeZeroMatcherType) { + return true + } + + if gexp.MatcherTypeIs(matcher.BeNumericallyMatcherType) { + mtchr := gexp.GetMatcherInfo().(*matcher.BeNumericallyMatcher) + return mtchr.GetOp() == token.EQL || mtchr.GetOp() == token.NEQ || gexp.MatcherTypeIs(matcher.EqualZero|matcher.GreaterThanZero) + } + } + + if gexp.ActualArgTypeIs(actual.CapComparisonActualArgType) && gexp.MatcherTypeIs(matcher.BeTrueMatcherType|matcher.BeFalseMatcherType|matcher.EqualBoolValueMatcherType) { + return true + } + + return false +} + +func (r *CapRule) fixExpression(gexp *expression.GomegaExpression) bool { + if gexp.ActualArgTypeIs(actual.CapFuncActualArgType) { + return r.fixEqual(gexp) + } + + if gexp.ActualArgTypeIs(actual.CapComparisonActualArgType) { + return r.fixComparison(gexp) + } + + return false +} + +func (r *CapRule) fixEqual(gexp *expression.GomegaExpression) bool { + matcherInfo := gexp.GetMatcherInfo() + switch mtchr := matcherInfo.(type) { + case *matcher.EqualMatcher: + gexp.SetMatcherCap(mtchr.GetValueExpr()) + + case *matcher.BeZeroMatcher: + gexp.SetMatcherCapZero() + + case *matcher.BeNumericallyMatcher: + if !r.handleBeNumerically(gexp, mtchr) { + return false + } + + default: + return false + } + + gexp.ReplaceActualWithItsFirstArg() + + return true +} + +func (r *CapRule) fixComparison(gexp *expression.GomegaExpression) bool { + actl := gexp.GetActualArg().(*actual.FuncComparisonPayload) + if op := actl.GetOp(); op == token.NEQ { + gexp.ReverseAssertionFuncLogic() + } else if op != token.EQL { + return false + } + + gexp.SetMatcherCap(actl.GetValueExpr()) + gexp.ReplaceActual(actl.GetFuncArg()) + + if gexp.MatcherTypeIs(matcher.BoolValueFalse) { + gexp.ReverseAssertionFuncLogic() + } + + return true +} + +func (r *CapRule) handleBeNumerically(gexp *expression.GomegaExpression, matcher *matcher.BeNumericallyMatcher) bool { + op := matcher.GetOp() + val := matcher.GetValue() + isValZero := val.String() == "0" + isValOne := val.String() == "1" + + if (op == token.GTR && isValZero) || (op == token.GEQ && isValOne) { + gexp.ReverseAssertionFuncLogic() + gexp.SetMatcherCapZero() + } else if op == token.EQL { + gexp.SetMatcherCap(matcher.GetValueExpr()) + } else if op == token.NEQ { + gexp.ReverseAssertionFuncLogic() + gexp.SetMatcherCap(matcher.GetValueExpr()) + } else { + return false + } + + return true +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/comparepointerrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/comparepointerrule.go new file mode 100644 index 000000000..dcbea1bc9 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/comparepointerrule.go @@ -0,0 +1,64 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const comparePointerToValue = "comparing a pointer to a value will always fail" + +type ComparePointRule struct{} + +func (r ComparePointRule) isApplied(gexp *expression.GomegaExpression) bool { + actl, ok := gexp.GetActualArg().(*actual.RegularArgPayload) + if !ok { + return false + } + + return actl.IsPointer() +} + +func (r ComparePointRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp) { + return false + } + + switch mtchr := gexp.GetMatcherInfo().(type) { + case *matcher.EqualMatcher: + if mtchr.IsPointer() || mtchr.IsInterface() { + return false + } + + case *matcher.BeEquivalentToMatcher: + if mtchr.IsPointer() || mtchr.IsInterface() || mtchr.IsNil() { + return false + } + + case *matcher.BeIdenticalToMatcher: + if mtchr.IsPointer() || mtchr.IsInterface() || mtchr.IsNil() { + return false + } + + case *matcher.EqualNilMatcher: + return false + + case *matcher.BeTrueMatcher, + *matcher.BeFalseMatcher, + *matcher.BeNumericallyMatcher, + *matcher.EqualTrueMatcher, + *matcher.EqualFalseMatcher: + + default: + return false + } + + getMatcherOnlyRules().Apply(gexp, config, reportBuilder) + + gexp.SetMatcherHaveValue() + reportBuilder.AddIssue(true, comparePointerToValue) + + return true +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/comparisonrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/comparisonrule.go new file mode 100644 index 000000000..fb38529e0 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/comparisonrule.go @@ -0,0 +1,75 @@ +package rules + +import ( + "go/token" + + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const wrongCompareWarningTemplate = "wrong comparison assertion" + +type ComparisonRule struct{} + +func (r ComparisonRule) isApplied(gexp *expression.GomegaExpression, config types.Config) bool { + if config.SuppressCompare { + return false + } + + return gexp.ActualArgTypeIs(actual.ComparisonActualArgType) +} + +func (r ComparisonRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp, config) { + return false + } + + actl, ok := gexp.GetActualArg().(actual.ComparisonActualPayload) + if !ok { + return false + } + + switch actl.GetOp() { + case token.EQL: + r.handleEqualComparison(gexp, actl) + + case token.NEQ: + gexp.ReverseAssertionFuncLogic() + r.handleEqualComparison(gexp, actl) + case token.GTR, token.GEQ, token.LSS, token.LEQ: + if !actl.GetRight().IsValueNumeric() { + return false + } + + gexp.SetMatcherBeNumerically(actl.GetOp(), actl.GetRight().GetValueExpr()) + + default: + return false + } + + if gexp.MatcherTypeIs(matcher.BoolValueFalse) { + gexp.ReverseAssertionFuncLogic() + } + + gexp.ReplaceActual(actl.GetLeft().GetValueExpr()) + + reportBuilder.AddIssue(true, wrongCompareWarningTemplate) + return true +} + +func (r ComparisonRule) handleEqualComparison(gexp *expression.GomegaExpression, actual actual.ComparisonActualPayload) { + if actual.GetRight().IsValueZero() { + gexp.SetMatcherBeZero() + } else { + left := actual.GetLeft() + arg := actual.GetRight().GetValueExpr() + if left.IsInterface() || left.IsPointer() { + gexp.SetMatcherBeIdenticalTo(arg) + } else { + gexp.SetMatcherEqual(arg) + } + } +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/doublenegativerule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/doublenegativerule.go new file mode 100644 index 000000000..6ce7be5a5 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/doublenegativerule.go @@ -0,0 +1,30 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const doubleNegativeWarningTemplate = "avoid double negative assertion" + +type DoubleNegativeRule struct{} + +func (DoubleNegativeRule) isApplied(gexp *expression.GomegaExpression) bool { + return gexp.MatcherTypeIs(matcher.BeFalseMatcherType) && + gexp.IsNegativeAssertion() +} + +func (r DoubleNegativeRule) Apply(gexp *expression.GomegaExpression, _ types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp) { + return false + } + + gexp.ReverseAssertionFuncLogic() + gexp.SetMatcherBeTrue() + + reportBuilder.AddIssue(true, doubleNegativeWarningTemplate) + + return true +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equalboolrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equalboolrule.go new file mode 100644 index 000000000..e9eaa1b80 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equalboolrule.go @@ -0,0 +1,36 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const wrongBoolWarningTemplate = "wrong boolean assertion" + +type EqualBoolRule struct{} + +func (r EqualBoolRule) isApplied(gexp *expression.GomegaExpression) bool { + return gexp.MatcherTypeIs(matcher.EqualBoolValueMatcherType) +} + +func (r EqualBoolRule) Apply(gexp *expression.GomegaExpression, _ types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp) { + return false + } + + if gexp.MatcherTypeIs(matcher.BoolValueTrue) { + gexp.SetMatcherBeTrue() + } else { + if gexp.IsNegativeAssertion() { + gexp.ReverseAssertionFuncLogic() + gexp.SetMatcherBeTrue() + } else { + gexp.SetMatcherBeFalse() + } + } + + reportBuilder.AddIssue(true, wrongBoolWarningTemplate) + return true +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equaldifferenttypesrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equaldifferenttypesrule.go new file mode 100644 index 000000000..81d703bb8 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equaldifferenttypesrule.go @@ -0,0 +1,119 @@ +package rules + +import ( + gotypes "go/types" + + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const compareDifferentTypes = "use %[1]s with different types: Comparing %[2]s with %[3]s; either change the expected value type if possible, or use the BeEquivalentTo() matcher, instead of %[1]s()" + +type EqualDifferentTypesRule struct{} + +func (r EqualDifferentTypesRule) isApplied(config types.Config) bool { + return !bool(config.SuppressTypeCompare) +} + +func (r EqualDifferentTypesRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(config) { + return false + } + + return r.checkEqualDifferentTypes(gexp, gexp.GetMatcher(), false, reportBuilder) +} + +func (r EqualDifferentTypesRule) checkEqualDifferentTypes(gexp *expression.GomegaExpression, mtchr *matcher.Matcher, parentPointer bool, reportBuilder *reports.Builder) bool { + actualType := gexp.GetActualArgGOType() + + if parentPointer { + if t, ok := actualType.(*gotypes.Pointer); ok { + actualType = t.Elem() + } + } + + var ( + matcherType gotypes.Type + matcherName string + ) + + switch specificMatcher := mtchr.GetMatcherInfo().(type) { + case *matcher.EqualMatcher: + matcherType = specificMatcher.GetType() + matcherName = specificMatcher.MatcherName() + + case *matcher.BeIdenticalToMatcher: + matcherType = specificMatcher.GetType() + matcherName = specificMatcher.MatcherName() + + case *matcher.HaveValueMatcher: + return r.checkEqualDifferentTypes(gexp, specificMatcher.GetNested(), true, reportBuilder) + + case *matcher.MultipleMatchersMatcher: + foundIssue := false + for i := range specificMatcher.Len() { + if r.checkEqualDifferentTypes(gexp, specificMatcher.At(i), parentPointer, reportBuilder) { + foundIssue = true + } + + } + return foundIssue + + case *matcher.EqualNilMatcher: + matcherType = specificMatcher.GetType() + matcherName = specificMatcher.MatcherName() + + case *matcher.WithTransformMatcher: + nested := specificMatcher.GetNested() + switch specificNested := nested.GetMatcherInfo().(type) { + case *matcher.EqualMatcher: + matcherType = specificNested.GetType() + matcherName = specificNested.MatcherName() + + case *matcher.BeIdenticalToMatcher: + matcherType = specificNested.GetType() + matcherName = specificNested.MatcherName() + + default: + return false + } + + actualType = specificMatcher.GetFuncType() + default: + return false + } + + if !gotypes.Identical(matcherType, actualType) { + if r.isImplementing(matcherType, actualType) || r.isImplementing(actualType, matcherType) { + return false + } + + reportBuilder.AddIssue(false, compareDifferentTypes, matcherName, actualType, matcherType) + return true + } + + return false +} + +func (r EqualDifferentTypesRule) isImplementing(ifs, impl gotypes.Type) bool { + if gotypes.IsInterface(ifs) { + + var ( + theIfs *gotypes.Interface + ok bool + ) + + for { + theIfs, ok = ifs.(*gotypes.Interface) + if ok { + break + } + ifs = ifs.Underlying() + } + + return gotypes.Implements(impl, theIfs) + } + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equalnilrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equalnilrule.go new file mode 100644 index 000000000..5b28e7d9b --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/equalnilrule.go @@ -0,0 +1,29 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +// EqualNilRule validate that there is no use of Equal(nil) in the code +// It is part of assertion only rules +type EqualNilRule struct{} + +func (r EqualNilRule) isApplied(gexp *expression.GomegaExpression, config types.Config) bool { + return !bool(config.SuppressNil) && + gexp.MatcherTypeIs(matcher.EqualValueMatcherType) +} + +func (r EqualNilRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp, config) { + return false + } + + gexp.SetMatcherBeNil() + + reportBuilder.AddIssue(true, wrongNilWarningTemplate) + + return true +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/errorequalnilrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/errorequalnilrule.go new file mode 100644 index 000000000..7aaf7631b --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/errorequalnilrule.go @@ -0,0 +1,35 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/expression/value" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +type ErrorEqualNilRule struct{} + +func (ErrorEqualNilRule) isApplied(gexp *expression.GomegaExpression, config types.Config) bool { + return !bool(config.SuppressErr) && + gexp.ActualArgTypeIs(actual.ErrorTypeArgType) && + gexp.MatcherTypeIs(matcher.BeNilMatcherType|matcher.EqualNilMatcherType) +} + +func (r ErrorEqualNilRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp, config) { + return false + } + + if v, ok := gexp.GetActualArg().(value.Valuer); ok && v.IsFunc() || gexp.ActualArgTypeIs(actual.ErrFuncActualArgType) { + gexp.SetMatcherSucceed() + } else { + gexp.ReverseAssertionFuncLogic() + gexp.SetMatcherHaveOccurred() + } + + reportBuilder.AddIssue(true, wrongErrWarningTemplate) + + return true +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/forceexpecttorule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/forceexpecttorule.go new file mode 100644 index 000000000..391d1d449 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/forceexpecttorule.go @@ -0,0 +1,43 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const forceExpectToTemplate = "must not use %s with %s" + +type ForceExpectToRule struct{} + +func (ForceExpectToRule) isApplied(gexp *expression.GomegaExpression, config types.Config) bool { + if !config.ForceExpectTo { + return false + } + + actlName := gexp.GetActualFuncName() + return actlName == "Expect" || actlName == "ExpectWithOffset" +} + +func (r ForceExpectToRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp, config) { + return false + } + + var newName string + + switch gexp.GetAssertFuncName() { + case "Should": + newName = "To" + case "ShouldNot": + newName = "ToNot" + default: + return false + } + + gexp.ReplaceAssertionMethod(newName) + reportBuilder.AddIssue(true, forceExpectToTemplate, gexp.GetActualFuncName(), gexp.GetOrigAssertFuncName()) + + // always return false, to keep checking another rules. + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/havelen0.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/havelen0.go new file mode 100644 index 000000000..20bcb7211 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/havelen0.go @@ -0,0 +1,23 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +type HaveLen0 struct{} + +func (r *HaveLen0) isApplied(gexp *expression.GomegaExpression, config types.Config) bool { + return gexp.MatcherTypeIs(matcher.HaveLenZeroMatcherType) && !bool(config.AllowHaveLen0) +} + +func (r *HaveLen0) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp, config) { + return false + } + gexp.SetMatcherBeEmpty() + reportBuilder.AddIssue(true, wrongLengthWarningTemplate) + return true +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/haveoccurredrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/haveoccurredrule.go new file mode 100644 index 000000000..437d3ee23 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/haveoccurredrule.go @@ -0,0 +1,35 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +type HaveOccurredRule struct{} + +func (r HaveOccurredRule) isApplied(gexp *expression.GomegaExpression) bool { + return gexp.MatcherTypeIs(matcher.HaveOccurredMatcherType) +} + +func (r HaveOccurredRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp) { + return false + } + + if !gexp.ActualArgTypeIs(actual.ErrorTypeArgType) { + reportBuilder.AddIssue(false, "asserting a non-error type with HaveOccurred matcher") + return true + } + + if bool(config.ForceSucceedForFuncs) && gexp.GetActualArg().(*actual.ErrPayload).IsFunc() { + gexp.ReverseAssertionFuncLogic() + gexp.SetMatcherSucceed() + reportBuilder.AddIssue(true, "prefer using the Succeed matcher for error function, instead of HaveOccurred") + return true + } + + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/lenrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/lenrule.go new file mode 100644 index 000000000..06d6f2c68 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/lenrule.go @@ -0,0 +1,119 @@ +package rules + +import ( + "go/token" + + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const wrongLengthWarningTemplate = "wrong length assertion" + +// LenRule does not allow using the len() function in actual with numeric comparison. Instead, +// it suggests to use the HaveLen matcher, or the BeEmpty matcher, if comparing to zero. +type LenRule struct{} + +func (r *LenRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + + if !r.isApplied(gexp, config) { + return false + } + + if r.fixExpression(gexp) { + reportBuilder.AddIssue(true, wrongLengthWarningTemplate) + return true + } + return false +} + +func (r *LenRule) isApplied(gexp *expression.GomegaExpression, config types.Config) bool { + if config.SuppressLen { + return false + } + + if gexp.ActualArgTypeIs(actual.LenFuncActualArgType) { + if gexp.MatcherTypeIs(matcher.EqualMatcherType | matcher.BeZeroMatcherType) { + return true + } + + if gexp.MatcherTypeIs(matcher.BeNumericallyMatcherType) { + mtchr := gexp.GetMatcherInfo().(*matcher.BeNumericallyMatcher) + return mtchr.GetOp() == token.EQL || mtchr.GetOp() == token.NEQ || gexp.MatcherTypeIs(matcher.EqualZero|matcher.GreaterThanZero) + } + } + + if gexp.ActualArgTypeIs(actual.LenComparisonActualArgType) && gexp.MatcherTypeIs(matcher.BeTrueMatcherType|matcher.BeFalseMatcherType|matcher.EqualBoolValueMatcherType) { + return true + } + + return false +} + +func (r *LenRule) fixExpression(gexp *expression.GomegaExpression) bool { + if gexp.ActualArgTypeIs(actual.LenFuncActualArgType) { + return r.fixEqual(gexp) + } + + if gexp.ActualArgTypeIs(actual.LenComparisonActualArgType) { + return r.fixComparison(gexp) + } + + return false +} + +func (r *LenRule) fixEqual(gexp *expression.GomegaExpression) bool { + + if gexp.MatcherTypeIs(matcher.EqualMatcherType) { + gexp.SetLenNumericMatcher() + + } else if gexp.MatcherTypeIs(matcher.BeZeroMatcherType) { + gexp.SetMatcherBeEmpty() + + } else if gexp.MatcherTypeIs(matcher.BeNumericallyMatcherType) { + mtchr := gexp.GetMatcherInfo().(*matcher.BeNumericallyMatcher) + op := mtchr.GetOp() + + if op == token.EQL { + gexp.SetLenNumericMatcher() + } else if op == token.NEQ { + gexp.ReverseAssertionFuncLogic() + gexp.SetLenNumericMatcher() + } else if gexp.MatcherTypeIs(matcher.GreaterThanZero) { + gexp.ReverseAssertionFuncLogic() + gexp.SetMatcherBeEmpty() + } else { + return false + } + } else { + return false + } + + gexp.ReplaceActualWithItsFirstArg() + return true +} + +func (r *LenRule) fixComparison(gexp *expression.GomegaExpression) bool { + actl := gexp.GetActualArg().(*actual.FuncComparisonPayload) + if op := actl.GetOp(); op == token.NEQ { + gexp.ReverseAssertionFuncLogic() + } else if op != token.EQL { + return false + } + + if gexp.MatcherTypeIs(matcher.BoolValueFalse) { + gexp.ReverseAssertionFuncLogic() + } + + if actl.IsValueZero() { + gexp.SetMatcherBeEmpty() + } else { + gexp.SetMatcherLen(actl.GetValueExpr()) + } + + gexp.ReplaceActual(actl.GetFuncArg()) + + return true +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/matcheronlyrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/matcheronlyrule.go new file mode 100644 index 000000000..1174393c6 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/matcheronlyrule.go @@ -0,0 +1,12 @@ +package rules + +var matcherOnlyRules = Rules{ + &HaveLen0{}, + &EqualBoolRule{}, + &EqualNilRule{}, + &DoubleNegativeRule{}, +} + +func getMatcherOnlyRules() Rules { + return matcherOnlyRules +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/matcherrorrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/matcherrorrule.go new file mode 100644 index 000000000..767b4b621 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/matcherrorrule.go @@ -0,0 +1,110 @@ +package rules + +import ( + "go/ast" + + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const ( + matchErrorArgWrongType = "the MatchError matcher used to assert a non error type (%s)" + matchErrorWrongTypeAssertion = "MatchError first parameter (%s) must be error, string, GomegaMatcher or func(error)bool are allowed" + matchErrorMissingDescription = "missing function description as second parameter of MatchError" + matchErrorRedundantArg = "redundant MatchError arguments; consider removing them" + matchErrorNoFuncDescription = "The second parameter of MatchError must be the function description (string)" +) + +// MatchErrorRule validates the usage of the MatchError matcher. +// +// # First, it checks that the actual value is actually an error +// +// Then, it checks the matcher itself: this matcher can be used in 3 different ways: +// 1. With error type variable +// 2. With another gomega matcher, to check the actual err.Error() value +// 3. With function with a signature of func(error) bool. In this case, additional description +// string variable is required. +type MatchErrorRule struct{} + +func (r MatchErrorRule) isApplied(gexp *expression.GomegaExpression) bool { + return gexp.MatcherTypeIs(matcher.MatchErrorMatcherType | matcher.MultipleMatcherMatherType) +} + +func (r MatchErrorRule) Apply(gexp *expression.GomegaExpression, _ types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp) { + return false + } + + return checkMatchError(gexp, reportBuilder) +} + +func checkMatchError(gexp *expression.GomegaExpression, reportBuilder *reports.Builder) bool { + mtchr := gexp.GetMatcherInfo() + switch m := mtchr.(type) { + case matcher.MatchErrorMatcher: + return checkMatchErrorMatcher(gexp, gexp.GetMatcher(), m, reportBuilder) + + case *matcher.MultipleMatchersMatcher: + res := false + for i := range m.Len() { + nested := m.At(i) + if specific, ok := nested.GetMatcherInfo().(matcher.MatchErrorMatcher); ok { + if valid := checkMatchErrorMatcher(gexp, gexp.GetMatcher(), specific, reportBuilder); valid { + res = true + } + } + } + return res + default: + return false + } +} + +func checkMatchErrorMatcher(gexp *expression.GomegaExpression, mtchr *matcher.Matcher, mtchrInfo matcher.MatchErrorMatcher, reportBuilder *reports.Builder) bool { + if !gexp.ActualArgTypeIs(actual.ErrorTypeArgType) { + reportBuilder.AddIssue(false, matchErrorArgWrongType, reportBuilder.FormatExpr(gexp.GetActualArgExpr())) + } + + switch m := mtchrInfo.(type) { + case *matcher.InvalidMatchErrorMatcher: + reportBuilder.AddIssue(false, matchErrorWrongTypeAssertion, reportBuilder.FormatExpr(mtchr.Clone.Args[0])) + + case *matcher.MatchErrorMatcherWithErrFunc: + if m.NumArgs() == m.AllowedNumArgs() { + if !m.IsSecondArgString() { + reportBuilder.AddIssue(false, matchErrorNoFuncDescription) + } + return true + } + + if m.NumArgs() == 1 { + reportBuilder.AddIssue(false, matchErrorMissingDescription) + return true + } + + case *matcher.MatchErrorMatcherWithErr, + *matcher.MatchErrorMatcherWithMatcher, + *matcher.MatchErrorMatcherWithString: + // continue + default: + return false + } + + if mtchrInfo.NumArgs() == mtchrInfo.AllowedNumArgs() { + return true + } + + if mtchrInfo.NumArgs() > mtchrInfo.AllowedNumArgs() { + var newArgsSuggestion []ast.Expr + for i := 0; i < mtchrInfo.AllowedNumArgs(); i++ { + newArgsSuggestion = append(newArgsSuggestion, mtchr.Clone.Args[i]) + } + mtchr.Clone.Args = newArgsSuggestion + reportBuilder.AddIssue(false, matchErrorRedundantArg) + return true + } + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/missingassertionrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/missingassertionrule.go new file mode 100644 index 000000000..43fc58bf6 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/missingassertionrule.go @@ -0,0 +1,27 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/gomegainfo" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const missingAssertionMessage = `%q: missing assertion method. Expected %s` + +type MissingAssertionRule struct{} + +func (r MissingAssertionRule) isApplied(gexp *expression.GomegaExpression) bool { + return gexp.IsMissingAssertion() +} + +func (r MissingAssertionRule) Apply(gexp *expression.GomegaExpression, _ types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp) { + return false + } + + actualMethodName := gexp.GetActualFuncName() + reportBuilder.AddIssue(false, missingAssertionMessage, actualMethodName, gomegainfo.GetAllowedAssertionMethods(actualMethodName)) + + return true +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/nilcomparerule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/nilcomparerule.go new file mode 100644 index 000000000..fc3cd49e5 --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/nilcomparerule.go @@ -0,0 +1,75 @@ +package rules + +import ( + "go/token" + + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +const ( + wrongNilWarningTemplate = "wrong nil assertion" + wrongErrWarningTemplate = "wrong error assertion" +) + +type NilCompareRule struct{} + +func (r NilCompareRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + isErr, ruleApplied := r.isApplied(gexp, config) + if !ruleApplied { + return false + } + + if gexp.MatcherTypeIs(matcher.BoolValueFalse) { + gexp.ReverseAssertionFuncLogic() + } + + r.handleNilBeBoolMatcher(gexp, gexp.GetActualArg().(*actual.NilComparisonPayload), reportBuilder, isErr) + + return true +} + +func (r NilCompareRule) isApplied(gexp *expression.GomegaExpression, config types.Config) (bool, bool) { + if !gexp.MatcherTypeIs(matcher.EqualBoolValueMatcherType | matcher.BeTrueMatcherType | matcher.BeFalseMatcherType) { + return false, false + } + + actl, ok := gexp.GetActualArg().(*actual.NilComparisonPayload) + if !ok { + return false, false + } + + isErr := actl.IsError() && !bool(config.SuppressErr) + + if !isErr && bool(config.SuppressNil) { + return isErr, false + } + + return isErr, true +} + +func (r NilCompareRule) handleNilBeBoolMatcher(gexp *expression.GomegaExpression, actl *actual.NilComparisonPayload, reportBuilder *reports.Builder, isErr bool) { + template := wrongNilWarningTemplate + if isErr { + template = wrongErrWarningTemplate + if actl.IsFunc() { + gexp.SetMatcherSucceed() + } else { + gexp.ReverseAssertionFuncLogic() + gexp.SetMatcherHaveOccurred() + } + } else { + gexp.SetMatcherBeNil() + } + + gexp.ReplaceActual(actl.GetValueExpr()) + + if actl.GetOp() == token.NEQ { + gexp.ReverseAssertionFuncLogic() + } + + reportBuilder.AddIssue(true, template) +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/rule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/rule.go new file mode 100644 index 000000000..cf331c21c --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/rule.go @@ -0,0 +1,61 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +type Rule interface { + Apply(*expression.GomegaExpression, types.Config, *reports.Builder) bool +} + +var rules = Rules{ + &ForceExpectToRule{}, + &LenRule{}, + &CapRule{}, + &ComparisonRule{}, + &NilCompareRule{}, + &ComparePointRule{}, + &ErrorEqualNilRule{}, + &MatchErrorRule{}, + getMatcherOnlyRules(), + &EqualDifferentTypesRule{}, + &HaveOccurredRule{}, + &SucceedRule{}, +} + +var asyncRules = Rules{ + &AsyncFuncCallRule{}, + &AsyncTimeIntervalsRule{}, + &ErrorEqualNilRule{}, + &MatchErrorRule{}, + &AsyncSucceedRule{}, + getMatcherOnlyRules(), +} + +func GetRules() Rules { + return rules +} + +func GetAsyncRules() Rules { + return asyncRules +} + +type Rules []Rule + +func (r Rules) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + for _, rule := range r { + if rule.Apply(gexp, config, reportBuilder) { + return true + } + } + + return false +} + +var missingAssertionRule = MissingAssertionRule{} + +func GetMissingAssertionRule() Rule { + return missingAssertionRule +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/succeedrule.go b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/succeedrule.go new file mode 100644 index 000000000..6a5167a8a --- /dev/null +++ b/vendor/github.com/nunnatsa/ginkgolinter/internal/rules/succeedrule.go @@ -0,0 +1,41 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/expression/actual" + "github.com/nunnatsa/ginkgolinter/internal/expression/matcher" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +type SucceedRule struct{} + +func (r SucceedRule) isApplied(gexp *expression.GomegaExpression) bool { + return !gexp.IsAsync() && gexp.MatcherTypeIs(matcher.SucceedMatcherType) +} + +func (r SucceedRule) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if !r.isApplied(gexp) { + return false + } + + if !gexp.ActualArgTypeIs(actual.ErrorTypeArgType) { + if gexp.IsActualTuple() { + reportBuilder.AddIssue(false, "the Success matcher does not support multiple values") + } else { + reportBuilder.AddIssue(false, "asserting a non-error type with Succeed matcher") + } + return true + } + + if bool(config.ForceSucceedForFuncs) && !gexp.GetActualArg().(*actual.ErrPayload).IsFunc() { + gexp.ReverseAssertionFuncLogic() + gexp.SetMatcherHaveOccurred() + + reportBuilder.AddIssue(true, "prefer using the HaveOccurred matcher for non-function error value, instead of Succeed") + + return true + } + + return false +} diff --git a/vendor/github.com/nunnatsa/ginkgolinter/linter/ginkgo_linter.go b/vendor/github.com/nunnatsa/ginkgolinter/linter/ginkgo_linter.go index 574fdfadf..188b2b5f9 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/linter/ginkgo_linter.go +++ b/vendor/github.com/nunnatsa/ginkgolinter/linter/ginkgo_linter.go @@ -1,24 +1,16 @@ package linter import ( - "bytes" - "fmt" "go/ast" - "go/constant" - "go/printer" - "go/token" - gotypes "go/types" - "reflect" - "github.com/go-toolsmith/astcopy" "golang.org/x/tools/go/analysis" + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/formatter" "github.com/nunnatsa/ginkgolinter/internal/ginkgohandler" "github.com/nunnatsa/ginkgolinter/internal/gomegahandler" - "github.com/nunnatsa/ginkgolinter/internal/interfaces" - "github.com/nunnatsa/ginkgolinter/internal/intervals" "github.com/nunnatsa/ginkgolinter/internal/reports" - "github.com/nunnatsa/ginkgolinter/internal/reverseassertion" + "github.com/nunnatsa/ginkgolinter/internal/rules" "github.com/nunnatsa/ginkgolinter/types" ) @@ -26,62 +18,6 @@ import ( // // For more details, look at the README.md file -const ( - linterName = "ginkgo-linter" - wrongLengthWarningTemplate = "wrong length assertion" - wrongCapWarningTemplate = "wrong cap assertion" - wrongNilWarningTemplate = "wrong nil assertion" - wrongBoolWarningTemplate = "wrong boolean assertion" - wrongErrWarningTemplate = "wrong error assertion" - wrongCompareWarningTemplate = "wrong comparison assertion" - doubleNegativeWarningTemplate = "avoid double negative assertion" - valueInEventually = "use a function call in %s. This actually checks nothing, because %s receives the function returned value, instead of function itself, and this value is never changed" - comparePointerToValue = "comparing a pointer to a value will always fail" - missingAssertionMessage = linterName + `: %q: missing assertion method. Expected %s` - focusContainerFound = linterName + ": Focus container found. This is used only for local debug and should not be part of the actual source code. Consider to replace with %q" - focusSpecFound = linterName + ": Focus spec found. This is used only for local debug and should not be part of the actual source code. Consider to remove it" - compareDifferentTypes = "use %[1]s with different types: Comparing %[2]s with %[3]s; either change the expected value type if possible, or use the BeEquivalentTo() matcher, instead of %[1]s()" - matchErrorArgWrongType = "the MatchError matcher used to assert a non error type (%s)" - matchErrorWrongTypeAssertion = "MatchError first parameter (%s) must be error, string, GomegaMatcher or func(error)bool are allowed" - matchErrorMissingDescription = "missing function description as second parameter of MatchError" - matchErrorRedundantArg = "redundant MatchError arguments; consider removing them" - matchErrorNoFuncDescription = "The second parameter of MatchError must be the function description (string)" - forceExpectToTemplate = "must not use Expect with %s" - useBeforeEachTemplate = "use BeforeEach() to assign variable %s" -) - -const ( // gomega matchers - beEmpty = "BeEmpty" - beEquivalentTo = "BeEquivalentTo" - beFalse = "BeFalse" - beIdenticalTo = "BeIdenticalTo" - beNil = "BeNil" - beNumerically = "BeNumerically" - beTrue = "BeTrue" - beZero = "BeZero" - equal = "Equal" - haveLen = "HaveLen" - haveCap = "HaveCap" - haveOccurred = "HaveOccurred" - haveValue = "HaveValue" - not = "Not" - omega = "Ω" - succeed = "Succeed" - and = "And" - or = "Or" - withTransform = "WithTransform" - matchError = "MatchError" -) - -const ( // gomega actuals - expect = "Expect" - expectWithOffset = "ExpectWithOffset" - eventually = "Eventually" - eventuallyWithOffset = "EventuallyWithOffset" - consistently = "Consistently" - consistentlyWithOffset = "ConsistentlyWithOffset" -) - type GinkgoLinter struct { config *types.Config } @@ -94,7 +30,7 @@ func NewGinkgoLinter(config *types.Config) *GinkgoLinter { } // Run is the main assertion function -func (l *GinkgoLinter) Run(pass *analysis.Pass) (interface{}, error) { +func (l *GinkgoLinter) Run(pass *analysis.Pass) (any, error) { for _, file := range pass.Files { fileConfig := l.config.Clone() @@ -102,39 +38,20 @@ func (l *GinkgoLinter) Run(pass *analysis.Pass) (interface{}, error) { fileConfig.UpdateFromFile(cm) - gomegaHndlr := gomegahandler.GetGomegaHandler(file) + gomegaHndlr := gomegahandler.GetGomegaHandler(file, pass) ginkgoHndlr := ginkgohandler.GetGinkgoHandler(file) if gomegaHndlr == nil && ginkgoHndlr == nil { // no gomega or ginkgo imports => no use in gomega in this file; nothing to do here continue } - timePks := "" - for _, imp := range file.Imports { - if imp.Path.Value == `"time"` { - if imp.Name == nil { - timePks = "time" - } else { - timePks = imp.Name.Name - } - } - } - ast.Inspect(file, func(n ast.Node) bool { if ginkgoHndlr != nil { goDeeper := false spec, ok := n.(*ast.ValueSpec) if ok { for _, val := range spec.Values { - if exp, ok := val.(*ast.CallExpr); ok { - if bool(fileConfig.ForbidFocus) && checkFocusContainer(pass, ginkgoHndlr, exp) { - goDeeper = true - } - - if bool(fileConfig.ForbidSpecPollution) && checkAssignmentsInContainer(pass, ginkgoHndlr, exp) { - goDeeper = true - } - } + goDeeper = ginkgoHndlr.HandleGinkgoSpecs(val, fileConfig, pass) || goDeeper } } if goDeeper { @@ -147,1527 +64,68 @@ func (l *GinkgoLinter) Run(pass *analysis.Pass) (interface{}, error) { return true } - config := fileConfig.Clone() - - if comments, ok := cm[stmt]; ok { - config.UpdateFromComment(comments) - } - // search for function calls assertionExp, ok := stmt.X.(*ast.CallExpr) if !ok { return true } + config := fileConfig.Clone() + if comments, ok := cm[stmt]; ok { + config.UpdateFromComment(comments) + } + if ginkgoHndlr != nil { - goDeeper := false - if bool(config.ForbidFocus) && checkFocusContainer(pass, ginkgoHndlr, assertionExp) { - goDeeper = true - } - if bool(config.ForbidSpecPollution) && checkAssignmentsInContainer(pass, ginkgoHndlr, assertionExp) { - goDeeper = true - } - if goDeeper { + if ginkgoHndlr.HandleGinkgoSpecs(assertionExp, config, pass) { return true } } - // no more ginkgo checks. From here it's only gomega. So if there is no gomega handler, exit here. This is - // mostly to prevent nil pointer error. + // no more ginkgo checks. From here it's only gomega. So if there is no gomega handler, exit here. if gomegaHndlr == nil { return true } - assertionFunc, ok := assertionExp.Fun.(*ast.SelectorExpr) - if !ok { - checkNoAssertion(pass, assertionExp, gomegaHndlr) - return true - } - - if !isAssertionFunc(assertionFunc.Sel.Name) { - checkNoAssertion(pass, assertionExp, gomegaHndlr) - return true - } - - actualExpr := gomegaHndlr.GetActualExpr(assertionFunc) - if actualExpr == nil { + gexp, ok := expression.New(assertionExp, pass, gomegaHndlr, getTimePkg(file)) + if !ok || gexp == nil { return true } - return checkExpression(pass, config, assertionExp, actualExpr, gomegaHndlr, timePks) + reportBuilder := reports.NewBuilder(assertionExp, formatter.NewGoFmtFormatter(pass.Fset)) + return checkGomegaExpression(gexp, config, reportBuilder, pass) }) } return nil, nil } -func checkAssignmentsInContainer(pass *analysis.Pass, ginkgoHndlr ginkgohandler.Handler, exp *ast.CallExpr) bool { - foundSomething := false - if ginkgoHndlr.IsWrapContainer(exp) { - for _, arg := range exp.Args { - if fn, ok := arg.(*ast.FuncLit); ok { - if fn.Body != nil { - if checkAssignments(pass, fn.Body.List) { - foundSomething = true - } - break - } - } - } - } - - return foundSomething -} - -func checkAssignments(pass *analysis.Pass, list []ast.Stmt) bool { - foundSomething := false - for _, stmt := range list { - switch st := stmt.(type) { - case *ast.DeclStmt: - if gen, ok := st.Decl.(*ast.GenDecl); ok { - if gen.Tok != token.VAR { - continue - } - for _, spec := range gen.Specs { - if valSpec, ok := spec.(*ast.ValueSpec); ok { - if checkAssignmentsValues(pass, valSpec.Names, valSpec.Values) { - foundSomething = true - } - } - } - } - - case *ast.AssignStmt: - for i, val := range st.Rhs { - if !is[*ast.FuncLit](val) { - if id, isIdent := st.Lhs[i].(*ast.Ident); isIdent && id.Name != "_" { - reportNoFix(pass, id.Pos(), useBeforeEachTemplate, id.Name) - foundSomething = true - } - } - } - - case *ast.IfStmt: - if st.Body != nil { - if checkAssignments(pass, st.Body.List) { - foundSomething = true - } - } - if st.Else != nil { - if block, isBlock := st.Else.(*ast.BlockStmt); isBlock { - if checkAssignments(pass, block.List) { - foundSomething = true - } - } - } - } - } - - return foundSomething -} - -func checkAssignmentsValues(pass *analysis.Pass, names []*ast.Ident, values []ast.Expr) bool { - foundSomething := false - for i, val := range values { - if !is[*ast.FuncLit](val) { - reportNoFix(pass, names[i].Pos(), useBeforeEachTemplate, names[i].Name) - foundSomething = true - } - } - - return foundSomething -} - -func checkFocusContainer(pass *analysis.Pass, ginkgoHndlr ginkgohandler.Handler, exp *ast.CallExpr) bool { - foundFocus := false - isFocus, id := ginkgoHndlr.GetFocusContainerName(exp) - if isFocus { - reportNewName(pass, id, id.Name[1:], focusContainerFound, id.Name) - foundFocus = true - } - - if id != nil && ginkgohandler.IsContainer(id.Name) { - for _, arg := range exp.Args { - if ginkgoHndlr.IsFocusSpec(arg) { - reportNoFix(pass, arg.Pos(), focusSpecFound) - foundFocus = true - } else if callExp, ok := arg.(*ast.CallExpr); ok { - if checkFocusContainer(pass, ginkgoHndlr, callExp) { // handle table entries - foundFocus = true - } - } - } - } - - return foundFocus -} - -func checkExpression(pass *analysis.Pass, config types.Config, assertionExp *ast.CallExpr, actualExpr *ast.CallExpr, handler gomegahandler.Handler, timePkg string) bool { - expr := astcopy.CallExpr(assertionExp) - - reportBuilder := reports.NewBuilder(pass.Fset, expr) - +func checkGomegaExpression(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder, pass *analysis.Pass) bool { goNested := false - if checkAsyncAssertion(pass, config, expr, actualExpr, handler, reportBuilder, timePkg) { + if rules.GetMissingAssertionRule().Apply(gexp, config, reportBuilder) { goNested = true } else { - - actualArg := getActualArg(actualExpr, handler) - if actualArg == nil { - return true - } - - if config.ForceExpectTo { - goNested = forceExpectTo(expr, handler, reportBuilder) || goNested + if gexp.IsAsync() { + rules.GetAsyncRules().Apply(gexp, config, reportBuilder) + goNested = true + } else { + rules.GetRules().Apply(gexp, config, reportBuilder) } - - goNested = doCheckExpression(pass, config, assertionExp, actualArg, expr, handler, reportBuilder) || goNested } if reportBuilder.HasReport() { - reportBuilder.SetFixOffer(pass.Fset, expr) + reportBuilder.SetFixOffer(gexp.GetClone()) pass.Report(reportBuilder.Build()) } return goNested } -func forceExpectTo(expr *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - if asrtFun, ok := expr.Fun.(*ast.SelectorExpr); ok { - if actualFuncName, ok := handler.GetActualFuncName(expr); ok && actualFuncName == expect { - var ( - name string - newIdent *ast.Ident - ) - - switch name = asrtFun.Sel.Name; name { - case "Should": - newIdent = ast.NewIdent("To") - case "ShouldNot": - newIdent = ast.NewIdent("ToNot") - default: - return false - } - - handler.ReplaceFunction(expr, newIdent) - reportBuilder.AddIssue(true, fmt.Sprintf(forceExpectToTemplate, name)) - return true - } - } - - return false -} - -func doCheckExpression(pass *analysis.Pass, config types.Config, assertionExp *ast.CallExpr, actualArg ast.Expr, expr *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - if !bool(config.SuppressLen) && isActualIsLenFunc(actualArg) { - return checkLengthMatcher(expr, pass, handler, reportBuilder) - - } else if !bool(config.SuppressLen) && isActualIsCapFunc(actualArg) { - return checkCapMatcher(expr, handler, reportBuilder) - - } else if nilable, compOp := getNilableFromComparison(actualArg); nilable != nil { - if isExprError(pass, nilable) { - if config.SuppressErr { - return true - } - } else if config.SuppressNil { - return true - } - - return checkNilMatcher(expr, pass, nilable, handler, compOp == token.NEQ, reportBuilder) - - } else if first, second, op, ok := isComparison(pass, actualArg); ok { - matcher, shouldContinue := startCheckComparison(expr, handler) - if !shouldContinue { - return false - } - if !config.SuppressLen { - if isActualIsLenFunc(first) { - if handleLenComparison(pass, expr, matcher, first, second, op, handler, reportBuilder) { - return false - } - } - if isActualIsCapFunc(first) { - if handleCapComparison(expr, matcher, first, second, op, handler, reportBuilder) { - return false - } - } - } - return bool(config.SuppressCompare) || checkComparison(expr, pass, matcher, handler, first, second, op, reportBuilder) - - } else if checkMatchError(pass, assertionExp, actualArg, handler, reportBuilder) { - return false - } else if isExprError(pass, actualArg) { - return bool(config.SuppressErr) || checkNilError(pass, expr, handler, actualArg, reportBuilder) - - } else if checkPointerComparison(pass, config, assertionExp, expr, actualArg, handler, reportBuilder) { - return false - } else if !handleAssertionOnly(pass, config, expr, handler, actualArg, reportBuilder) { - return false - } else if !config.SuppressTypeCompare { - return !checkEqualWrongType(pass, assertionExp, actualArg, handler, reportBuilder) - } - - return true -} - -func checkMatchError(pass *analysis.Pass, origExp *ast.CallExpr, actualArg ast.Expr, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - matcher, ok := origExp.Args[0].(*ast.CallExpr) - if !ok { - return false - } - - return doCheckMatchError(pass, origExp, matcher, actualArg, handler, reportBuilder) -} - -func doCheckMatchError(pass *analysis.Pass, origExp *ast.CallExpr, matcher *ast.CallExpr, actualArg ast.Expr, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - name, ok := handler.GetActualFuncName(matcher) - if !ok { - return false - } - switch name { - case matchError: - case not: - nested, ok := matcher.Args[0].(*ast.CallExpr) - if !ok { - return false - } - - return doCheckMatchError(pass, origExp, nested, actualArg, handler, reportBuilder) - case and, or: - res := false - for _, arg := range matcher.Args { - if nested, ok := arg.(*ast.CallExpr); ok { - if valid := doCheckMatchError(pass, origExp, nested, actualArg, handler, reportBuilder); valid { - res = true - } - } - } - return res - default: - return false - } - - if !isExprError(pass, actualArg) { - reportBuilder.AddIssue(false, matchErrorArgWrongType, goFmt(pass.Fset, actualArg)) - } - - expr := astcopy.CallExpr(matcher) - - validAssertion, requiredParams := checkMatchErrorAssertion(pass, matcher) - if !validAssertion { - reportBuilder.AddIssue(false, matchErrorWrongTypeAssertion, goFmt(pass.Fset, matcher.Args[0])) - } - - numParams := len(matcher.Args) - if numParams == requiredParams { - if numParams == 2 { - t := pass.TypesInfo.TypeOf(matcher.Args[1]) - if !gotypes.Identical(t, gotypes.Typ[gotypes.String]) { - reportBuilder.AddIssue(false, matchErrorNoFuncDescription) - return true - } - } - return true - } - - if requiredParams == 2 && numParams == 1 { - reportBuilder.AddIssue(false, matchErrorMissingDescription) - return true - } - - var newArgsSuggestion = []ast.Expr{expr.Args[0]} - if requiredParams == 2 { - newArgsSuggestion = append(newArgsSuggestion, expr.Args[1]) - } - expr.Args = newArgsSuggestion - - reportBuilder.AddIssue(true, matchErrorRedundantArg) - return true -} - -func checkMatchErrorAssertion(pass *analysis.Pass, matcher *ast.CallExpr) (bool, int) { - if isErrorMatcherValidArg(pass, matcher.Args[0]) { - return true, 1 - } - - t1 := pass.TypesInfo.TypeOf(matcher.Args[0]) - if isFuncErrBool(t1) { - return true, 2 - } - - return false, 0 -} - -// isFuncErrBool checks if a function is with the signature `func(error) bool` -func isFuncErrBool(t gotypes.Type) bool { - sig, ok := t.(*gotypes.Signature) - if !ok { - return false - } - if sig.Params().Len() != 1 || sig.Results().Len() != 1 { - return false - } - - if !interfaces.ImplementsError(sig.Params().At(0).Type()) { - return false - } - - b, ok := sig.Results().At(0).Type().(*gotypes.Basic) - if ok && b.Name() == "bool" && b.Info() == gotypes.IsBoolean && b.Kind() == gotypes.Bool { - return true - } - - return false -} - -func isErrorMatcherValidArg(pass *analysis.Pass, arg ast.Expr) bool { - if isExprError(pass, arg) { - return true - } - - if t, ok := pass.TypesInfo.TypeOf(arg).(*gotypes.Basic); ok && t.Kind() == gotypes.String { - return true - } - - t := pass.TypesInfo.TypeOf(arg) - - return interfaces.ImplementsGomegaMatcher(t) -} - -func checkEqualWrongType(pass *analysis.Pass, origExp *ast.CallExpr, actualArg ast.Expr, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - matcher, ok := origExp.Args[0].(*ast.CallExpr) - if !ok { - return false - } - - return checkEqualDifferentTypes(pass, matcher, actualArg, handler, false, reportBuilder) -} - -func checkEqualDifferentTypes(pass *analysis.Pass, matcher *ast.CallExpr, actualArg ast.Expr, handler gomegahandler.Handler, parentPointer bool, reportBuilder *reports.Builder) bool { - matcherFuncName, ok := handler.GetActualFuncName(matcher) - if !ok { - return false - } - - actualType := pass.TypesInfo.TypeOf(actualArg) - - switch matcherFuncName { - case equal, beIdenticalTo: // continue - case and, or: - foundIssue := false - for _, nestedExp := range matcher.Args { - nested, ok := nestedExp.(*ast.CallExpr) - if !ok { - continue - } - if checkEqualDifferentTypes(pass, nested, actualArg, handler, parentPointer, reportBuilder) { - foundIssue = true - } - } - - return foundIssue - case withTransform: - nested, ok := matcher.Args[1].(*ast.CallExpr) - if !ok { - return false - } - - matcherFuncName, ok = handler.GetActualFuncName(nested) - switch matcherFuncName { - case equal, beIdenticalTo: - case not: - return checkEqualDifferentTypes(pass, nested, actualArg, handler, parentPointer, reportBuilder) - default: - return false - } - - if t := getFuncType(pass, matcher.Args[0]); t != nil { - actualType = t - matcher = nested - - if !ok { - return false - } - } else { - return checkEqualDifferentTypes(pass, nested, actualArg, handler, parentPointer, reportBuilder) - } - - case not: - nested, ok := matcher.Args[0].(*ast.CallExpr) - if !ok { - return false - } - - return checkEqualDifferentTypes(pass, nested, actualArg, handler, parentPointer, reportBuilder) - - case haveValue: - nested, ok := matcher.Args[0].(*ast.CallExpr) - if !ok { - return false - } - - return checkEqualDifferentTypes(pass, nested, actualArg, handler, true, reportBuilder) - default: - return false - } - - matcherValue := matcher.Args[0] - - switch act := actualType.(type) { - case *gotypes.Tuple: - actualType = act.At(0).Type() - case *gotypes.Pointer: - if parentPointer { - actualType = act.Elem() - } - } - - matcherType := pass.TypesInfo.TypeOf(matcherValue) - - if !reflect.DeepEqual(matcherType, actualType) { - // Equal can handle comparison of interface and a value that implements it - if isImplementing(matcherType, actualType) || isImplementing(actualType, matcherType) { - return false - } - - reportBuilder.AddIssue(false, compareDifferentTypes, matcherFuncName, actualType, matcherType) - return true - } - - return false -} - -func getFuncType(pass *analysis.Pass, expr ast.Expr) gotypes.Type { - switch f := expr.(type) { - case *ast.FuncLit: - if f.Type != nil && f.Type.Results != nil && len(f.Type.Results.List) > 0 { - return pass.TypesInfo.TypeOf(f.Type.Results.List[0].Type) - } - case *ast.Ident: - a := pass.TypesInfo.TypeOf(f) - if sig, ok := a.(*gotypes.Signature); ok && sig.Results().Len() > 0 { - return sig.Results().At(0).Type() - } - } - - return nil -} - -func isImplementing(ifs, impl gotypes.Type) bool { - if gotypes.IsInterface(ifs) { - - var ( - theIfs *gotypes.Interface - ok bool - ) - - for { - theIfs, ok = ifs.(*gotypes.Interface) - if ok { - break - } - ifs = ifs.Underlying() - } - - return gotypes.Implements(impl, theIfs) - } - return false -} - -// be careful - never change origExp!!! only modify its clone, expr!!! -func checkPointerComparison(pass *analysis.Pass, config types.Config, origExp *ast.CallExpr, expr *ast.CallExpr, actualArg ast.Expr, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - if !isPointer(pass, actualArg) { - return false - } - matcher, ok := origExp.Args[0].(*ast.CallExpr) - if !ok { - return false - } - - matcherFuncName, ok := handler.GetActualFuncName(matcher) - if !ok { - return false - } - - // not using recurse here, since we need the original expression, in order to get the TypeInfo, while we should not - // modify it. - for matcherFuncName == not { - reverseAssertionFuncLogic(expr) - expr.Args[0] = expr.Args[0].(*ast.CallExpr).Args[0] - matcher, ok = matcher.Args[0].(*ast.CallExpr) - if !ok { - return false - } - - matcherFuncName, ok = handler.GetActualFuncName(matcher) - if !ok { - return false - } - } - - switch matcherFuncName { - case equal, beIdenticalTo, beEquivalentTo: - arg := matcher.Args[0] - if isPointer(pass, arg) { - return false - } - if isNil(arg) { - return false - } - if isInterface(pass, arg) { - return false - } - case beFalse, beTrue, beNumerically: - default: - return false - } - - handleAssertionOnly(pass, config, expr, handler, actualArg, reportBuilder) - - args := []ast.Expr{astcopy.CallExpr(expr.Args[0].(*ast.CallExpr))} - handler.ReplaceFunction(expr.Args[0].(*ast.CallExpr), ast.NewIdent(haveValue)) - expr.Args[0].(*ast.CallExpr).Args = args - - reportBuilder.AddIssue(true, comparePointerToValue) - return true -} - -// check async assertion does not assert function call. This is a real bug in the test. In this case, the assertion is -// done on the returned value, instead of polling the result of a function, for instance. -func checkAsyncAssertion(pass *analysis.Pass, config types.Config, expr *ast.CallExpr, actualExpr *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder, timePkg string) bool { - funcName, ok := handler.GetActualFuncName(actualExpr) - if !ok { - return false - } - - var funcIndex int - switch funcName { - case eventually, consistently: - funcIndex = 0 - case eventuallyWithOffset, consistentlyWithOffset: - funcIndex = 1 - default: - return false - } - - if !config.SuppressAsync && len(actualExpr.Args) > funcIndex { - t := pass.TypesInfo.TypeOf(actualExpr.Args[funcIndex]) - - // skip context variable, if used as first argument - if "context.Context" == t.String() { - funcIndex++ - } - - if len(actualExpr.Args) > funcIndex { - if fun, funcCall := actualExpr.Args[funcIndex].(*ast.CallExpr); funcCall { - t = pass.TypesInfo.TypeOf(fun) - if !isValidAsyncValueType(t) { - actualExpr = handler.GetActualExpr(expr.Fun.(*ast.SelectorExpr)) - - if len(fun.Args) > 0 { - origArgs := actualExpr.Args - origFunc := actualExpr.Fun - actualExpr.Args = fun.Args - - origArgs[funcIndex] = fun.Fun - call := &ast.SelectorExpr{ - Sel: ast.NewIdent("WithArguments"), - X: &ast.CallExpr{ - Fun: origFunc, - Args: origArgs, - }, - } - - actualExpr.Fun = call - actualExpr.Args = fun.Args - actualExpr = actualExpr.Fun.(*ast.SelectorExpr).X.(*ast.CallExpr) - } else { - actualExpr.Args[funcIndex] = fun.Fun - } - - reportBuilder.AddIssue(true, valueInEventually, funcName, funcName) - } - } - } - - if config.ValidateAsyncIntervals { - intervals.CheckIntervals(pass, expr, actualExpr, reportBuilder, handler, timePkg, funcIndex) +func getTimePkg(file *ast.File) string { + timePkg := "time" + for _, imp := range file.Imports { + if imp.Path.Value == `"time"` && imp.Name != nil { + timePkg = imp.Name.Name } } - handleAssertionOnly(pass, config, expr, handler, actualExpr, reportBuilder) - return true -} - -func isValidAsyncValueType(t gotypes.Type) bool { - switch t.(type) { - // allow functions that return function or channel. - case *gotypes.Signature, *gotypes.Chan, *gotypes.Pointer: - return true - case *gotypes.Named: - return isValidAsyncValueType(t.Underlying()) - } - - return false -} - -func startCheckComparison(exp *ast.CallExpr, handler gomegahandler.Handler) (*ast.CallExpr, bool) { - matcher, ok := exp.Args[0].(*ast.CallExpr) - if !ok { - return nil, false - } - - matcherFuncName, ok := handler.GetActualFuncName(matcher) - if !ok { - return nil, false - } - - switch matcherFuncName { - case beTrue: - case beFalse: - reverseAssertionFuncLogic(exp) - case equal: - boolean, found := matcher.Args[0].(*ast.Ident) - if !found { - return nil, false - } - - if boolean.Name == "false" { - reverseAssertionFuncLogic(exp) - } else if boolean.Name != "true" { - return nil, false - } - - case not: - reverseAssertionFuncLogic(exp) - exp.Args[0] = exp.Args[0].(*ast.CallExpr).Args[0] - return startCheckComparison(exp, handler) - - default: - return nil, false - } - - return matcher, true -} - -func checkComparison(exp *ast.CallExpr, pass *analysis.Pass, matcher *ast.CallExpr, handler gomegahandler.Handler, first ast.Expr, second ast.Expr, op token.Token, reportBuilder *reports.Builder) bool { - fun, ok := exp.Fun.(*ast.SelectorExpr) - if !ok { - return true - } - - call := handler.GetActualExpr(fun) - if call == nil { - return true - } - - switch op { - case token.EQL: - handleEqualComparison(pass, matcher, first, second, handler) - - case token.NEQ: - reverseAssertionFuncLogic(exp) - handleEqualComparison(pass, matcher, first, second, handler) - case token.GTR, token.GEQ, token.LSS, token.LEQ: - if !isNumeric(pass, first) { - return true - } - handler.ReplaceFunction(matcher, ast.NewIdent(beNumerically)) - matcher.Args = []ast.Expr{ - &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf(`"%s"`, op.String())}, - second, - } - default: - return true - } - - call.Args = []ast.Expr{first} - reportBuilder.AddIssue(true, wrongCompareWarningTemplate) - return false -} - -func handleEqualComparison(pass *analysis.Pass, matcher *ast.CallExpr, first ast.Expr, second ast.Expr, handler gomegahandler.Handler) { - if isZero(pass, second) { - handler.ReplaceFunction(matcher, ast.NewIdent(beZero)) - matcher.Args = nil - } else { - t := pass.TypesInfo.TypeOf(first) - if gotypes.IsInterface(t) { - handler.ReplaceFunction(matcher, ast.NewIdent(beIdenticalTo)) - } else if is[*gotypes.Pointer](t) { - handler.ReplaceFunction(matcher, ast.NewIdent(beIdenticalTo)) - } else { - handler.ReplaceFunction(matcher, ast.NewIdent(equal)) - } - - matcher.Args = []ast.Expr{second} - } -} - -func handleLenComparison(pass *analysis.Pass, exp *ast.CallExpr, matcher *ast.CallExpr, first ast.Expr, second ast.Expr, op token.Token, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - switch op { - case token.EQL: - case token.NEQ: - reverseAssertionFuncLogic(exp) - default: - return false - } - - var eql *ast.Ident - if isZero(pass, second) { - eql = ast.NewIdent(beEmpty) - } else { - eql = ast.NewIdent(haveLen) - matcher.Args = []ast.Expr{second} - } - - handler.ReplaceFunction(matcher, eql) - firstLen, ok := first.(*ast.CallExpr) // assuming it's len() - if !ok { - return false // should never happen - } - - val := firstLen.Args[0] - fun := handler.GetActualExpr(exp.Fun.(*ast.SelectorExpr)) - fun.Args = []ast.Expr{val} - - reportBuilder.AddIssue(true, wrongLengthWarningTemplate) - return true -} - -func handleCapComparison(exp *ast.CallExpr, matcher *ast.CallExpr, first ast.Expr, second ast.Expr, op token.Token, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - switch op { - case token.EQL: - case token.NEQ: - reverseAssertionFuncLogic(exp) - default: - return false - } - - eql := ast.NewIdent(haveCap) - matcher.Args = []ast.Expr{second} - - handler.ReplaceFunction(matcher, eql) - firstLen, ok := first.(*ast.CallExpr) // assuming it's len() - if !ok { - return false // should never happen - } - - val := firstLen.Args[0] - fun := handler.GetActualExpr(exp.Fun.(*ast.SelectorExpr)) - fun.Args = []ast.Expr{val} - - reportBuilder.AddIssue(true, wrongCapWarningTemplate) - return true -} - -// Check if the "actual" argument is a call to the golang built-in len() function -func isActualIsLenFunc(actualArg ast.Expr) bool { - return checkActualFuncName(actualArg, "len") -} - -// Check if the "actual" argument is a call to the golang built-in len() function -func isActualIsCapFunc(actualArg ast.Expr) bool { - return checkActualFuncName(actualArg, "cap") -} - -func checkActualFuncName(actualArg ast.Expr, name string) bool { - lenArgExp, ok := actualArg.(*ast.CallExpr) - if !ok { - return false - } - - lenFunc, ok := lenArgExp.Fun.(*ast.Ident) - return ok && lenFunc.Name == name -} - -// Check if matcher function is in one of the patterns we want to avoid -func checkLengthMatcher(exp *ast.CallExpr, pass *analysis.Pass, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - matcher, ok := exp.Args[0].(*ast.CallExpr) - if !ok { - return true - } - - matcherFuncName, ok := handler.GetActualFuncName(matcher) - if !ok { - return true - } - - switch matcherFuncName { - case equal: - handleEqualLenMatcher(matcher, pass, exp, handler, reportBuilder) - return false - - case beZero: - handleBeZero(exp, handler, reportBuilder) - return false - - case beNumerically: - return handleBeNumerically(matcher, pass, exp, handler, reportBuilder) - - case not: - reverseAssertionFuncLogic(exp) - exp.Args[0] = exp.Args[0].(*ast.CallExpr).Args[0] - return checkLengthMatcher(exp, pass, handler, reportBuilder) - - default: - return true - } -} - -// Check if matcher function is in one of the patterns we want to avoid -func checkCapMatcher(exp *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - matcher, ok := exp.Args[0].(*ast.CallExpr) - if !ok { - return true - } - - matcherFuncName, ok := handler.GetActualFuncName(matcher) - if !ok { - return true - } - - switch matcherFuncName { - case equal: - handleEqualCapMatcher(matcher, exp, handler, reportBuilder) - return false - - case beZero: - handleCapBeZero(exp, handler, reportBuilder) - return false - - case beNumerically: - return handleCapBeNumerically(matcher, exp, handler, reportBuilder) - - case not: - reverseAssertionFuncLogic(exp) - exp.Args[0] = exp.Args[0].(*ast.CallExpr).Args[0] - return checkCapMatcher(exp, handler, reportBuilder) - - default: - return true - } -} - -// Check if matcher function is in one of the patterns we want to avoid -func checkNilMatcher(exp *ast.CallExpr, pass *analysis.Pass, nilable ast.Expr, handler gomegahandler.Handler, notEqual bool, reportBuilder *reports.Builder) bool { - matcher, ok := exp.Args[0].(*ast.CallExpr) - if !ok { - return true - } - - matcherFuncName, ok := handler.GetActualFuncName(matcher) - if !ok { - return true - } - - switch matcherFuncName { - case equal: - handleEqualNilMatcher(matcher, pass, exp, handler, nilable, notEqual, reportBuilder) - - case beTrue: - handleNilBeBoolMatcher(pass, exp, handler, nilable, notEqual, reportBuilder) - - case beFalse: - reverseAssertionFuncLogic(exp) - handleNilBeBoolMatcher(pass, exp, handler, nilable, notEqual, reportBuilder) - - case not: - reverseAssertionFuncLogic(exp) - exp.Args[0] = exp.Args[0].(*ast.CallExpr).Args[0] - return checkNilMatcher(exp, pass, nilable, handler, notEqual, reportBuilder) - - default: - return true - } - return false -} - -func checkNilError(pass *analysis.Pass, assertionExp *ast.CallExpr, handler gomegahandler.Handler, actualArg ast.Expr, reportBuilder *reports.Builder) bool { - if len(assertionExp.Args) == 0 { - return true - } - - equalFuncExpr, ok := assertionExp.Args[0].(*ast.CallExpr) - if !ok { - return true - } - - funcName, ok := handler.GetActualFuncName(equalFuncExpr) - if !ok { - return true - } - - switch funcName { - case beNil: // no additional processing needed. - case equal: - - if len(equalFuncExpr.Args) == 0 { - return true - } - - nilable, ok := equalFuncExpr.Args[0].(*ast.Ident) - if !ok || nilable.Name != "nil" { - return true - } - - case not: - reverseAssertionFuncLogic(assertionExp) - assertionExp.Args[0] = assertionExp.Args[0].(*ast.CallExpr).Args[0] - return checkNilError(pass, assertionExp, handler, actualArg, reportBuilder) - default: - return true - } - - var newFuncName string - if is[*ast.CallExpr](actualArg) { - newFuncName = succeed - } else { - reverseAssertionFuncLogic(assertionExp) - newFuncName = haveOccurred - } - - handler.ReplaceFunction(equalFuncExpr, ast.NewIdent(newFuncName)) - equalFuncExpr.Args = nil - - reportBuilder.AddIssue(true, wrongErrWarningTemplate) - return false -} - -// handleAssertionOnly checks use-cases when the actual value is valid, but only the assertion should be fixed -// it handles: -// -// Equal(nil) => BeNil() -// Equal(true) => BeTrue() -// Equal(false) => BeFalse() -// HaveLen(0) => BeEmpty() -func handleAssertionOnly(pass *analysis.Pass, config types.Config, expr *ast.CallExpr, handler gomegahandler.Handler, actualArg ast.Expr, reportBuilder *reports.Builder) bool { - if len(expr.Args) == 0 { - return true - } - - equalFuncExpr, ok := expr.Args[0].(*ast.CallExpr) - if !ok { - return true - } - - funcName, ok := handler.GetActualFuncName(equalFuncExpr) - if !ok { - return true - } - - switch funcName { - case equal: - if len(equalFuncExpr.Args) == 0 { - return true - } - - tkn, ok := equalFuncExpr.Args[0].(*ast.Ident) - if !ok { - return true - } - - var replacement string - var template string - switch tkn.Name { - case "nil": - if config.SuppressNil { - return true - } - replacement = beNil - template = wrongNilWarningTemplate - case "true": - replacement = beTrue - template = wrongBoolWarningTemplate - case "false": - if isNegativeAssertion(expr) { - reverseAssertionFuncLogic(expr) - replacement = beTrue - } else { - replacement = beFalse - } - template = wrongBoolWarningTemplate - default: - return true - } - - handler.ReplaceFunction(equalFuncExpr, ast.NewIdent(replacement)) - equalFuncExpr.Args = nil - - reportBuilder.AddIssue(true, template) - return false - - case beFalse: - if isNegativeAssertion(expr) { - reverseAssertionFuncLogic(expr) - handler.ReplaceFunction(equalFuncExpr, ast.NewIdent(beTrue)) - reportBuilder.AddIssue(true, doubleNegativeWarningTemplate) - return false - } - return false - - case haveLen: - if config.AllowHaveLen0 { - return true - } - - if len(equalFuncExpr.Args) > 0 { - if isZero(pass, equalFuncExpr.Args[0]) { - handler.ReplaceFunction(equalFuncExpr, ast.NewIdent(beEmpty)) - equalFuncExpr.Args = nil - reportBuilder.AddIssue(true, wrongLengthWarningTemplate) - return false - } - } - - return true - - case not: - reverseAssertionFuncLogic(expr) - expr.Args[0] = expr.Args[0].(*ast.CallExpr).Args[0] - return handleAssertionOnly(pass, config, expr, handler, actualArg, reportBuilder) - default: - return true - } -} - -func isZero(pass *analysis.Pass, arg ast.Expr) bool { - if val, ok := arg.(*ast.BasicLit); ok && val.Kind == token.INT && val.Value == "0" { - return true - } - info, ok := pass.TypesInfo.Types[arg] - if ok { - if t, ok := info.Type.(*gotypes.Basic); ok && t.Kind() == gotypes.Int && info.Value != nil { - if i, ok := constant.Int64Val(info.Value); ok && i == 0 { - return true - } - } - } else if val, ok := arg.(*ast.Ident); ok && val.Obj != nil && val.Obj.Kind == ast.Con { - if spec, ok := val.Obj.Decl.(*ast.ValueSpec); ok { - if len(spec.Values) == 1 { - if value, ok := spec.Values[0].(*ast.BasicLit); ok && value.Kind == token.INT && value.Value == "0" { - return true - } - } - } - } - - return false -} - -// getActualArg checks that the function is an assertion's actual function and return the "actual" parameter. If the -// function is not assertion's actual function, return nil. -func getActualArg(actualExpr *ast.CallExpr, handler gomegahandler.Handler) ast.Expr { - funcName, ok := handler.GetActualFuncName(actualExpr) - if !ok { - return nil - } - - switch funcName { - case expect, omega: - return actualExpr.Args[0] - case expectWithOffset: - return actualExpr.Args[1] - default: - return nil - } -} - -// Replace the len function call by its parameter, to create a fix suggestion -func replaceLenActualArg(actualExpr *ast.CallExpr, handler gomegahandler.Handler) { - name, ok := handler.GetActualFuncName(actualExpr) - if !ok { - return - } - - switch name { - case expect, omega: - arg := actualExpr.Args[0] - if isActualIsLenFunc(arg) || isActualIsCapFunc(arg) { - // replace the len function call by its parameter, to create a fix suggestion - actualExpr.Args[0] = arg.(*ast.CallExpr).Args[0] - } - case expectWithOffset: - arg := actualExpr.Args[1] - if isActualIsLenFunc(arg) || isActualIsCapFunc(arg) { - // replace the len function call by its parameter, to create a fix suggestion - actualExpr.Args[1] = arg.(*ast.CallExpr).Args[0] - } - } -} - -// Replace the nil comparison with the compared object, to create a fix suggestion -func replaceNilActualArg(actualExpr *ast.CallExpr, handler gomegahandler.Handler, nilable ast.Expr) bool { - actualFuncName, ok := handler.GetActualFuncName(actualExpr) - if !ok { - return false - } - - switch actualFuncName { - case expect, omega: - actualExpr.Args[0] = nilable - return true - - case expectWithOffset: - actualExpr.Args[1] = nilable - return true - - default: - return false - } -} - -// For the BeNumerically matcher, we want to avoid the assertion of length to be > 0 or >= 1, or just == number -func handleBeNumerically(matcher *ast.CallExpr, pass *analysis.Pass, exp *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - opExp, ok1 := matcher.Args[0].(*ast.BasicLit) - valExp, ok2 := matcher.Args[1].(*ast.BasicLit) - - if ok1 && ok2 { - op := opExp.Value - val := valExp.Value - - if (op == `">"` && val == "0") || (op == `">="` && val == "1") { - reverseAssertionFuncLogic(exp) - handler.ReplaceFunction(exp.Args[0].(*ast.CallExpr), ast.NewIdent(beEmpty)) - exp.Args[0].(*ast.CallExpr).Args = nil - } else if op == `"=="` { - chooseNumericMatcher(pass, exp, handler, valExp) - } else if op == `"!="` { - reverseAssertionFuncLogic(exp) - chooseNumericMatcher(pass, exp, handler, valExp) - } else { - return true - } - - reportLengthAssertion(exp, handler, reportBuilder) - return false - } - return true -} - -// For the BeNumerically matcher, we want to avoid the assertion of length to be > 0 or >= 1, or just == number -func handleCapBeNumerically(matcher *ast.CallExpr, exp *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) bool { - opExp, ok1 := matcher.Args[0].(*ast.BasicLit) - valExp, ok2 := matcher.Args[1].(*ast.BasicLit) - - if ok1 && ok2 { - op := opExp.Value - val := valExp.Value - - if (op == `">"` && val == "0") || (op == `">="` && val == "1") { - reverseAssertionFuncLogic(exp) - handler.ReplaceFunction(exp.Args[0].(*ast.CallExpr), ast.NewIdent(haveCap)) - exp.Args[0].(*ast.CallExpr).Args = []ast.Expr{&ast.BasicLit{Kind: token.INT, Value: "0"}} - } else if op == `"=="` { - replaceNumericCapMatcher(exp, handler, valExp) - } else if op == `"!="` { - reverseAssertionFuncLogic(exp) - replaceNumericCapMatcher(exp, handler, valExp) - } else { - return true - } - - reportCapAssertion(exp, handler, reportBuilder) - return false - } - return true -} - -func chooseNumericMatcher(pass *analysis.Pass, exp *ast.CallExpr, handler gomegahandler.Handler, valExp ast.Expr) { - caller := exp.Args[0].(*ast.CallExpr) - if isZero(pass, valExp) { - handler.ReplaceFunction(caller, ast.NewIdent(beEmpty)) - exp.Args[0].(*ast.CallExpr).Args = nil - } else { - handler.ReplaceFunction(caller, ast.NewIdent(haveLen)) - exp.Args[0].(*ast.CallExpr).Args = []ast.Expr{valExp} - } -} - -func replaceNumericCapMatcher(exp *ast.CallExpr, handler gomegahandler.Handler, valExp ast.Expr) { - caller := exp.Args[0].(*ast.CallExpr) - handler.ReplaceFunction(caller, ast.NewIdent(haveCap)) - exp.Args[0].(*ast.CallExpr).Args = []ast.Expr{valExp} -} - -func reverseAssertionFuncLogic(exp *ast.CallExpr) { - assertionFunc := exp.Fun.(*ast.SelectorExpr).Sel - assertionFunc.Name = reverseassertion.ChangeAssertionLogic(assertionFunc.Name) -} - -func isNegativeAssertion(exp *ast.CallExpr) bool { - assertionFunc := exp.Fun.(*ast.SelectorExpr).Sel - return reverseassertion.IsNegativeLogic(assertionFunc.Name) -} - -func handleEqualLenMatcher(matcher *ast.CallExpr, pass *analysis.Pass, exp *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) { - equalTo, ok := matcher.Args[0].(*ast.BasicLit) - if ok { - chooseNumericMatcher(pass, exp, handler, equalTo) - } else { - handler.ReplaceFunction(exp.Args[0].(*ast.CallExpr), ast.NewIdent(haveLen)) - exp.Args[0].(*ast.CallExpr).Args = []ast.Expr{matcher.Args[0]} - } - reportLengthAssertion(exp, handler, reportBuilder) -} - -func handleEqualCapMatcher(matcher *ast.CallExpr, exp *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) { - handler.ReplaceFunction(exp.Args[0].(*ast.CallExpr), ast.NewIdent(haveCap)) - exp.Args[0].(*ast.CallExpr).Args = []ast.Expr{matcher.Args[0]} - reportCapAssertion(exp, handler, reportBuilder) -} - -func handleBeZero(exp *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) { - exp.Args[0].(*ast.CallExpr).Args = nil - handler.ReplaceFunction(exp.Args[0].(*ast.CallExpr), ast.NewIdent(beEmpty)) - reportLengthAssertion(exp, handler, reportBuilder) -} - -func handleCapBeZero(exp *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) { - exp.Args[0].(*ast.CallExpr).Args = nil - handler.ReplaceFunction(exp.Args[0].(*ast.CallExpr), ast.NewIdent(haveCap)) - exp.Args[0].(*ast.CallExpr).Args = []ast.Expr{&ast.BasicLit{Kind: token.INT, Value: "0"}} - reportCapAssertion(exp, handler, reportBuilder) -} - -func handleEqualNilMatcher(matcher *ast.CallExpr, pass *analysis.Pass, exp *ast.CallExpr, handler gomegahandler.Handler, nilable ast.Expr, notEqual bool, reportBuilder *reports.Builder) { - equalTo, ok := matcher.Args[0].(*ast.Ident) - if !ok { - return - } - - if equalTo.Name == "false" { - reverseAssertionFuncLogic(exp) - } else if equalTo.Name != "true" { - return - } - - newFuncName, isItError := handleNilComparisonErr(pass, exp, nilable) - - handler.ReplaceFunction(exp.Args[0].(*ast.CallExpr), ast.NewIdent(newFuncName)) - exp.Args[0].(*ast.CallExpr).Args = nil - - reportNilAssertion(exp, handler, nilable, notEqual, isItError, reportBuilder) -} - -func handleNilBeBoolMatcher(pass *analysis.Pass, exp *ast.CallExpr, handler gomegahandler.Handler, nilable ast.Expr, notEqual bool, reportBuilder *reports.Builder) { - newFuncName, isItError := handleNilComparisonErr(pass, exp, nilable) - handler.ReplaceFunction(exp.Args[0].(*ast.CallExpr), ast.NewIdent(newFuncName)) - exp.Args[0].(*ast.CallExpr).Args = nil - - reportNilAssertion(exp, handler, nilable, notEqual, isItError, reportBuilder) -} - -func handleNilComparisonErr(pass *analysis.Pass, exp *ast.CallExpr, nilable ast.Expr) (string, bool) { - newFuncName := beNil - isItError := isExprError(pass, nilable) - if isItError { - if is[*ast.CallExpr](nilable) { - newFuncName = succeed - } else { - reverseAssertionFuncLogic(exp) - newFuncName = haveOccurred - } - } - - return newFuncName, isItError -} - -func isAssertionFunc(name string) bool { - switch name { - case "To", "ToNot", "NotTo", "Should", "ShouldNot": - return true - } - return false -} - -func reportLengthAssertion(expr *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) { - actualExpr := handler.GetActualExpr(expr.Fun.(*ast.SelectorExpr)) - replaceLenActualArg(actualExpr, handler) - - reportBuilder.AddIssue(true, wrongLengthWarningTemplate) -} - -func reportCapAssertion(expr *ast.CallExpr, handler gomegahandler.Handler, reportBuilder *reports.Builder) { - actualExpr := handler.GetActualExpr(expr.Fun.(*ast.SelectorExpr)) - replaceLenActualArg(actualExpr, handler) - - reportBuilder.AddIssue(true, wrongCapWarningTemplate) -} - -func reportNilAssertion(expr *ast.CallExpr, handler gomegahandler.Handler, nilable ast.Expr, notEqual bool, isItError bool, reportBuilder *reports.Builder) { - actualExpr := handler.GetActualExpr(expr.Fun.(*ast.SelectorExpr)) - changed := replaceNilActualArg(actualExpr, handler, nilable) - if !changed { - return - } - - if notEqual { - reverseAssertionFuncLogic(expr) - } - template := wrongNilWarningTemplate - if isItError { - template = wrongErrWarningTemplate - } - - reportBuilder.AddIssue(true, template) -} - -func reportNewName(pass *analysis.Pass, id *ast.Ident, newName string, messageTemplate, oldExpr string) { - pass.Report(analysis.Diagnostic{ - Pos: id.Pos(), - Message: fmt.Sprintf(messageTemplate, newName), - SuggestedFixes: []analysis.SuggestedFix{ - { - Message: fmt.Sprintf("should replace %s with %s", oldExpr, newName), - TextEdits: []analysis.TextEdit{ - { - Pos: id.Pos(), - End: id.End(), - NewText: []byte(newName), - }, - }, - }, - }, - }) -} - -func reportNoFix(pass *analysis.Pass, pos token.Pos, message string, args ...any) { - if len(args) > 0 { - message = fmt.Sprintf(message, args...) - } - - pass.Report(analysis.Diagnostic{ - Pos: pos, - Message: message, - }) -} - -func getNilableFromComparison(actualArg ast.Expr) (ast.Expr, token.Token) { - bin, ok := actualArg.(*ast.BinaryExpr) - if !ok { - return nil, token.ILLEGAL - } - - if bin.Op == token.EQL || bin.Op == token.NEQ { - if isNil(bin.Y) { - return bin.X, bin.Op - } else if isNil(bin.X) { - return bin.Y, bin.Op - } - } - - return nil, token.ILLEGAL -} - -func isNil(expr ast.Expr) bool { - nilObject, ok := expr.(*ast.Ident) - return ok && nilObject.Name == "nil" && nilObject.Obj == nil -} - -func isComparison(pass *analysis.Pass, actualArg ast.Expr) (ast.Expr, ast.Expr, token.Token, bool) { - bin, ok := actualArg.(*ast.BinaryExpr) - if !ok { - return nil, nil, token.ILLEGAL, false - } - - first, second, op := bin.X, bin.Y, bin.Op - replace := false - switch realFirst := first.(type) { - case *ast.Ident: // check if const - info, ok := pass.TypesInfo.Types[realFirst] - if ok { - if is[*gotypes.Basic](info.Type) && info.Value != nil { - replace = true - } - } - - case *ast.BasicLit: - replace = true - } - - if replace { - first, second = second, first - } - - switch op { - case token.EQL: - case token.NEQ: - case token.GTR, token.GEQ, token.LSS, token.LEQ: - if replace { - op = reverseassertion.ChangeCompareOperator(op) - } - default: - return nil, nil, token.ILLEGAL, false - } - return first, second, op, true -} - -func goFmt(fset *token.FileSet, x ast.Expr) string { - var b bytes.Buffer - _ = printer.Fprint(&b, fset, x) - return b.String() -} - -func isExprError(pass *analysis.Pass, expr ast.Expr) bool { - actualArgType := pass.TypesInfo.TypeOf(expr) - switch t := actualArgType.(type) { - case *gotypes.Named: - if interfaces.ImplementsError(actualArgType) { - return true - } - case *gotypes.Tuple: - if t.Len() > 0 { - switch t0 := t.At(0).Type().(type) { - case *gotypes.Named, *gotypes.Pointer: - if interfaces.ImplementsError(t0) { - return true - } - } - } - } - return false -} - -func isPointer(pass *analysis.Pass, expr ast.Expr) bool { - t := pass.TypesInfo.TypeOf(expr) - return is[*gotypes.Pointer](t) -} - -func isInterface(pass *analysis.Pass, expr ast.Expr) bool { - t := pass.TypesInfo.TypeOf(expr) - return gotypes.IsInterface(t) -} - -func isNumeric(pass *analysis.Pass, node ast.Expr) bool { - t := pass.TypesInfo.TypeOf(node) - - switch t.String() { - case "int", "uint", "int8", "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64", "float32", "float64": - return true - } - return false -} - -func checkNoAssertion(pass *analysis.Pass, expr *ast.CallExpr, handler gomegahandler.Handler) { - funcName, ok := handler.GetActualFuncName(expr) - if ok { - var allowedFunction string - switch funcName { - case expect, expectWithOffset: - allowedFunction = `"To()", "ToNot()" or "NotTo()"` - case eventually, eventuallyWithOffset, consistently, consistentlyWithOffset: - allowedFunction = `"Should()" or "ShouldNot()"` - case omega: - allowedFunction = `"Should()", "To()", "ShouldNot()", "ToNot()" or "NotTo()"` - default: - return - } - reportNoFix(pass, expr.Pos(), missingAssertionMessage, funcName, allowedFunction) - } -} - -func is[T any](x any) bool { - _, matchType := x.(T) - return matchType + return timePkg } diff --git a/vendor/github.com/nunnatsa/ginkgolinter/types/config.go b/vendor/github.com/nunnatsa/ginkgolinter/types/config.go index b6838e524..0aadd3416 100644 --- a/vendor/github.com/nunnatsa/ginkgolinter/types/config.go +++ b/vendor/github.com/nunnatsa/ginkgolinter/types/config.go @@ -28,6 +28,7 @@ type Config struct { ForceExpectTo Boolean ValidateAsyncIntervals Boolean ForbidSpecPollution Boolean + ForceSucceedForFuncs Boolean } func (s *Config) AllTrue() bool { @@ -47,6 +48,7 @@ func (s *Config) Clone() Config { ForceExpectTo: s.ForceExpectTo, ValidateAsyncIntervals: s.ValidateAsyncIntervals, ForbidSpecPollution: s.ForbidSpecPollution, + ForceSucceedForFuncs: s.ForceSucceedForFuncs, } } -- cgit mrf-deployment